diff options
Diffstat (limited to 'org.eclipse.jgit/src')
193 files changed, 7317 insertions, 4216 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java index f7576e9e9b..a69aa70c6e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/BlameCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, GitHub Inc. + * Copyright (C) 2011, 2019 GitHub Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -42,11 +42,7 @@ */ package org.eclipse.jgit.api; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -56,17 +52,10 @@ import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.blame.BlameGenerator; import org.eclipse.jgit.blame.BlameResult; import org.eclipse.jgit.diff.DiffAlgorithm; -import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.diff.RawTextComparator; -import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.lib.AnyObjectId; -import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.CoreConfig.AutoCRLF; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.treewalk.WorkingTreeOptions; -import org.eclipse.jgit.util.IO; -import org.eclipse.jgit.util.io.AutoLFInputStream; /** * Blame command for building a {@link org.eclipse.jgit.blame.BlameResult} for a @@ -221,69 +210,11 @@ public class BlameCommand extends GitCommand<BlameResult> { else if (startCommit != null) gen.push(null, startCommit); else { - gen.push(null, repo.resolve(Constants.HEAD)); - if (!repo.isBare()) { - DirCache dc = repo.readDirCache(); - int entry = dc.findEntry(path); - if (0 <= entry) - gen.push(null, dc.getEntry(entry).getObjectId()); - - File inTree = new File(repo.getWorkTree(), path); - if (repo.getFS().isFile(inTree)) { - RawText rawText = getRawText(inTree); - gen.push(null, rawText); - } - } + gen.prepareHead(); } return gen.computeBlameResult(); } catch (IOException e) { throw new JGitInternalException(e.getMessage(), e); } } - - private RawText getRawText(File inTree) throws IOException, - FileNotFoundException { - RawText rawText; - - WorkingTreeOptions workingTreeOptions = getRepository().getConfig() - .get(WorkingTreeOptions.KEY); - AutoCRLF autoCRLF = workingTreeOptions.getAutoCRLF(); - switch (autoCRLF) { - case FALSE: - case INPUT: - // Git used the repo format on checkout, but other tools - // may change the format to CRLF. We ignore that here. - rawText = new RawText(inTree); - break; - case TRUE: - try (AutoLFInputStream in = new AutoLFInputStream( - new FileInputStream(inTree), true)) { - // Canonicalization should lead to same or shorter length - // (CRLF to LF), so the file size on disk is an upper size bound - rawText = new RawText(toByteArray(in, (int) inTree.length())); - } - break; - default: - throw new IllegalArgumentException( - "Unknown autocrlf option " + autoCRLF); //$NON-NLS-1$ - } - return rawText; - } - - private static byte[] toByteArray(InputStream source, int upperSizeLimit) - throws IOException { - byte[] buffer = new byte[upperSizeLimit]; - try { - int read = IO.readFully(source, buffer, 0); - if (read == upperSizeLimit) - return buffer; - else { - byte[] copy = new byte[read]; - System.arraycopy(buffer, 0, copy, 0, read); - return copy; - } - } finally { - source.close(); - } - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java index e05f6f1bd6..6d157bd0a1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java @@ -137,7 +137,7 @@ public class CheckoutCommand extends GitCommand<Ref> { /** * Stage to check out, see {@link CheckoutCommand#setStage(Stage)}. */ - public static enum Stage { + public enum Stage { /** * Base stage (#1) */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java index c9dd547b49..aa63725c55 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java @@ -57,6 +57,7 @@ import org.eclipse.jgit.api.errors.UnmergedPathsException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.events.WorkingTreeModifiedEvent; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; @@ -129,9 +130,10 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { // get the head commit Ref headRef = repo.exactRef(Constants.HEAD); - if (headRef == null) + if (headRef == null) { throw new NoHeadException( JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported); + } newHead = revWalk.parseCommit(headRef.getObjectId()); @@ -140,8 +142,9 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { // get the commit to be cherry-picked // handle annotated tags ObjectId srcObjectId = src.getPeeledObjectId(); - if (srcObjectId == null) + if (srcObjectId == null) { srcObjectId = src.getObjectId(); + } RevCommit srcCommit = revWalk.parseCommit(srcObjectId); // get the parent of the commit to cherry-pick @@ -157,26 +160,33 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { merger.setCommitNames(new String[] { "BASE", ourName, //$NON-NLS-1$ cherryPickName }); if (merger.merge(newHead, srcCommit)) { + if (!merger.getModifiedFiles().isEmpty()) { + repo.fireEvent(new WorkingTreeModifiedEvent( + merger.getModifiedFiles(), null)); + } if (AnyObjectId.isEqual(newHead.getTree().getId(), - merger.getResultTreeId())) + merger.getResultTreeId())) { continue; + } DirCacheCheckout dco = new DirCacheCheckout(repo, newHead.getTree(), repo.lockDirCache(), merger.getResultTreeId()); dco.setFailOnConflict(true); dco.setProgressMonitor(monitor); dco.checkout(); - if (!noCommit) + if (!noCommit) { newHead = new Git(getRepository()).commit() .setMessage(srcCommit.getFullMessage()) .setReflogComment(reflogPrefix + " " //$NON-NLS-1$ + srcCommit.getShortMessage()) .setAuthor(srcCommit.getAuthorIdent()) .setNoVerify(true).call(); + } cherryPickedRefs.add(src); } else { - if (merger.failed()) + if (merger.failed()) { return new CherryPickResult(merger.getFailingPaths()); + } // there are merge conflicts @@ -184,10 +194,14 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { .formatWithConflicts(srcCommit.getFullMessage(), merger.getUnmergedPaths()); - if (!noCommit) + if (!noCommit) { repo.writeCherryPickHead(srcCommit.getId()); + } repo.writeMergeCommitMsg(message); + repo.fireEvent(new WorkingTreeModifiedEvent( + merger.getModifiedFiles(), null)); + return CherryPickResult.CONFLICT; } } @@ -213,10 +227,11 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { Integer.valueOf(srcCommit.getParentCount()))); srcParent = srcCommit.getParent(0); } else { - if (mainlineParentNumber.intValue() > srcCommit.getParentCount()) + if (mainlineParentNumber.intValue() > srcCommit.getParentCount()) { throw new JGitInternalException(MessageFormat.format( JGitText.get().commitDoesNotHaveGivenParent, srcCommit, mainlineParentNumber)); + } srcParent = srcCommit .getParent(mainlineParentNumber.intValue() - 1); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java index 9f63d0f005..7008cd49a8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java @@ -77,8 +77,8 @@ import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.TagOpt; import org.eclipse.jgit.transport.URIish; -import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FileUtils; /** * Clone a repository into a new working directory @@ -106,6 +106,8 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { private boolean cloneAllBranches; + private boolean mirror; + private boolean cloneSubmodules; private boolean noCheckout; @@ -118,6 +120,12 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { private boolean gitDirExistsInitially; + private FETCH_TYPE fetchType; + + private enum FETCH_TYPE { + MULTIPLE_BRANCHES, ALL_BRANCHES, MIRROR + } + /** * Callback for status of clone operation. * @@ -191,6 +199,7 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { throw new InvalidRemoteException( MessageFormat.format(JGitText.get().invalidURL, uri)); } + setFetchType(); @SuppressWarnings("resource") // Closed by caller Repository repository = init(); FetchResult fetchResult = null; @@ -234,6 +243,20 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { return new Git(repository, true); } + private void setFetchType() { + if (mirror) { + fetchType = FETCH_TYPE.MIRROR; + setBare(true); + } else if (cloneAllBranches) { + fetchType = FETCH_TYPE.ALL_BRANCHES; + } else if (branchesToClone != null && !branchesToClone.isEmpty()) { + fetchType = FETCH_TYPE.MULTIPLE_BRANCHES; + } else { + // Default: neither mirror nor all nor specific refs given + fetchType = FETCH_TYPE.ALL_BRANCHES; + } + } + private static boolean isNonEmptyDirectory(File dir) { if (dir != null && dir.exists()) { File[] files = dir.listFiles(); @@ -282,12 +305,11 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { RemoteConfig config = new RemoteConfig(clonedRepo.getConfig(), remote); config.addURI(u); - final String dst = (bare ? Constants.R_HEADS : Constants.R_REMOTES - + config.getName() + '/') + '*'; - boolean fetchAll = cloneAllBranches || branchesToClone == null - || branchesToClone.isEmpty(); + boolean fetchAll = fetchType == FETCH_TYPE.ALL_BRANCHES + || fetchType == FETCH_TYPE.MIRROR; - config.setFetchRefSpecs(calculateRefSpecs(fetchAll, dst)); + config.setFetchRefSpecs(calculateRefSpecs(fetchType, config.getName())); + config.setMirror(fetchType == FETCH_TYPE.MIRROR); config.update(clonedRepo.getConfig()); clonedRepo.getConfig().save(); @@ -302,26 +324,33 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { return command.call(); } - private List<RefSpec> calculateRefSpecs(boolean fetchAll, String dst) { - RefSpec heads = new RefSpec(); - heads = heads.setForceUpdate(true); - heads = heads.setSourceDestination(Constants.R_HEADS + '*', dst); + private List<RefSpec> calculateRefSpecs(FETCH_TYPE type, + String remoteName) { List<RefSpec> specs = new ArrayList<>(); - if (!fetchAll) { - RefSpec tags = new RefSpec(); - tags = tags.setForceUpdate(true); - tags = tags.setSourceDestination(Constants.R_TAGS + '*', - Constants.R_TAGS + '*'); - for (String selectedRef : branchesToClone) { - if (heads.matchSource(selectedRef)) { - specs.add(heads.expandFromSource(selectedRef)); - } else if (tags.matchSource(selectedRef)) { - specs.add(tags.expandFromSource(selectedRef)); + if (type == FETCH_TYPE.MIRROR) { + specs.add(new RefSpec().setForceUpdate(true).setSourceDestination( + Constants.R_REFS + '*', Constants.R_REFS + '*')); + } else { + RefSpec heads = new RefSpec(); + heads = heads.setForceUpdate(true); + final String dst = (bare ? Constants.R_HEADS + : Constants.R_REMOTES + remoteName + '/') + '*'; + heads = heads.setSourceDestination(Constants.R_HEADS + '*', dst); + if (type == FETCH_TYPE.MULTIPLE_BRANCHES) { + RefSpec tags = new RefSpec().setForceUpdate(true) + .setSourceDestination(Constants.R_TAGS + '*', + Constants.R_TAGS + '*'); + for (String selectedRef : branchesToClone) { + if (heads.matchSource(selectedRef)) { + specs.add(heads.expandFromSource(selectedRef)); + } else if (tags.matchSource(selectedRef)) { + specs.add(tags.expandFromSource(selectedRef)); + } } + } else { + // We'll fetch the tags anyway. + specs.add(heads); } - } else { - // We'll fetch the tags anyway. - specs.add(heads); } return specs; } @@ -614,6 +643,26 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { } /** + * Set up a mirror of the source repository. This implies that a bare + * repository will be created. Compared to {@link #setBare}, + * {@code #setMirror} not only maps local branches of the source to local + * branches of the target, it maps all refs (including remote-tracking + * branches, notes etc.) and sets up a refspec configuration such that all + * these refs are overwritten by a git remote update in the target + * repository. + * + * @param mirror + * whether to mirror all refs from the source repository + * + * @return {@code this} + * @since 5.6 + */ + public CloneCommand setMirror(boolean mirror) { + this.mirror = mirror; + return this; + } + + /** * Set whether to clone submodules * * @param cloneSubmodules @@ -630,8 +679,9 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> { * Set the branches or tags to clone. * <p> * This is ignored if {@link #setCloneAllBranches(boolean) - * setCloneAllBranches(true)} is used. If {@code branchesToClone} is - * {@code null} or empty, it's also ignored and all branches will be cloned. + * setCloneAllBranches(true)} or {@link #setMirror(boolean) setMirror(true)} + * is used. If {@code branchesToClone} is {@code null} or empty, it's also + * ignored. * </p> * * @param branchesToClone diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index b55987ead4..b32f7ab20b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -143,6 +143,8 @@ public class CommitCommand extends GitCommand<RevCommit> { private HashMap<String, PrintStream> hookOutRedirect = new HashMap<>(3); + private HashMap<String, PrintStream> hookErrRedirect = new HashMap<>(3); + private Boolean allowEmpty; private Boolean signCommit; @@ -188,7 +190,8 @@ public class CommitCommand extends GitCommand<RevCommit> { state.name())); if (!noVerify) { - Hooks.preCommit(repo, hookOutRedirect.get(PreCommitHook.NAME)) + Hooks.preCommit(repo, hookOutRedirect.get(PreCommitHook.NAME), + hookErrRedirect.get(PreCommitHook.NAME)) .call(); } @@ -230,7 +233,8 @@ public class CommitCommand extends GitCommand<RevCommit> { if (!noVerify) { message = Hooks .commitMsg(repo, - hookOutRedirect.get(CommitMsgHook.NAME)) + hookOutRedirect.get(CommitMsgHook.NAME), + hookErrRedirect.get(CommitMsgHook.NAME)) .setCommitMessage(message).call(); } @@ -311,7 +315,8 @@ public class CommitCommand extends GitCommand<RevCommit> { repo.writeRevertHead(null); } Hooks.postCommit(repo, - hookOutRedirect.get(PostCommitHook.NAME)).call(); + hookOutRedirect.get(PostCommitHook.NAME), + hookErrRedirect.get(PostCommitHook.NAME)).call(); return revCommit; } @@ -519,7 +524,7 @@ public class CommitCommand extends GitCommand<RevCommit> { int position = Collections.binarySearch(only, p); if (position >= 0) return position; - int l = p.lastIndexOf("/"); //$NON-NLS-1$ + int l = p.lastIndexOf('/'); if (l < 1) break; p = p.substring(0, l); @@ -891,6 +896,23 @@ public class CommitCommand extends GitCommand<RevCommit> { } /** + * Set the error stream for all hook scripts executed by this command + * (pre-commit, commit-msg, post-commit). If not set it defaults to + * {@code System.err}. + * + * @param hookStdErr + * the error stream for hook scripts executed by this command + * @return {@code this} + * @since 5.6 + */ + public CommitCommand setHookErrorStream(PrintStream hookStdErr) { + setHookErrorStream(PreCommitHook.NAME, hookStdErr); + setHookErrorStream(CommitMsgHook.NAME, hookStdErr); + setHookErrorStream(PostCommitHook.NAME, hookStdErr); + return this; + } + + /** * Set the output stream for a selected hook script executed by this command * (pre-commit, commit-msg, post-commit). If not set it defaults to * {@code System.out}. @@ -916,6 +938,30 @@ public class CommitCommand extends GitCommand<RevCommit> { } /** + * Set the error stream for a selected hook script executed by this command + * (pre-commit, commit-msg, post-commit). If not set it defaults to + * {@code System.err}. + * + * @param hookName + * name of the hook to set the output stream for + * @param hookStdErr + * the output stream to use for the selected hook + * @return {@code this} + * @since 5.6 + */ + public CommitCommand setHookErrorStream(String hookName, + PrintStream hookStdErr) { + if (!(PreCommitHook.NAME.equals(hookName) + || CommitMsgHook.NAME.equals(hookName) + || PostCommitHook.NAME.equals(hookName))) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().illegalHookName, hookName)); + } + hookErrRedirect.put(hookName, hookStdErr); + return this; + } + + /** * Sets the signing key * <p> * Per spec of user.signingKey: this will be sent to the GPG program as is, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DiffCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DiffCommand.java index f65b5735de..1c3c79041e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DiffCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DiffCommand.java @@ -136,28 +136,32 @@ public class DiffCommand extends GitCommand<List<DiffEntry>> { } newTree = new DirCacheIterator(repo.readDirCache()); } else { - if (oldTree == null) + if (oldTree == null) { oldTree = new DirCacheIterator(repo.readDirCache()); - if (newTree == null) + } + if (newTree == null) { newTree = new FileTreeIterator(repo); + } } diffFmt.setPathFilter(pathFilter); List<DiffEntry> result = diffFmt.scan(oldTree, newTree); - if (showNameAndStatusOnly) - return result; - else { - if (contextLines >= 0) - diffFmt.setContext(contextLines); - if (destinationPrefix != null) - diffFmt.setNewPrefix(destinationPrefix); - if (sourcePrefix != null) - diffFmt.setOldPrefix(sourcePrefix); - diffFmt.format(result); - diffFmt.flush(); + if (showNameAndStatusOnly) { return result; } + if (contextLines >= 0) { + diffFmt.setContext(contextLines); + } + if (destinationPrefix != null) { + diffFmt.setNewPrefix(destinationPrefix); + } + if (sourcePrefix != null) { + diffFmt.setOldPrefix(sourcePrefix); + } + diffFmt.format(result); + diffFmt.flush(); + return result; } catch (IOException e) { throw new JGitInternalException(e.getMessage(), e); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java index 2c9c5f20cc..9020c58d46 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java @@ -360,17 +360,17 @@ public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> { * @return whether to remove refs which no longer exist in the source */ public boolean isRemoveDeletedRefs() { - if (removeDeletedRefs != null) + if (removeDeletedRefs != null) { return removeDeletedRefs.booleanValue(); - else { // fall back to configuration - boolean result = false; - StoredConfig config = repo.getConfig(); - result = config.getBoolean(ConfigConstants.CONFIG_FETCH_SECTION, - null, ConfigConstants.CONFIG_KEY_PRUNE, result); - result = config.getBoolean(ConfigConstants.CONFIG_REMOTE_SECTION, - remote, ConfigConstants.CONFIG_KEY_PRUNE, result); - return result; } + // fall back to configuration + boolean result = false; + StoredConfig config = repo.getConfig(); + result = config.getBoolean(ConfigConstants.CONFIG_FETCH_SECTION, null, + ConfigConstants.CONFIG_KEY_PRUNE, result); + result = config.getBoolean(ConfigConstants.CONFIG_REMOTE_SECTION, + remote, ConfigConstants.CONFIG_KEY_PRUNE, result); + return result; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java index 7ea277157d..474e2f5736 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/GarbageCollectCommand.java @@ -243,9 +243,8 @@ public class GarbageCollectCommand extends GitCommand<Properties> { if (repo instanceof FileRepository) { GC gc = new GC((FileRepository) repo); return toProperties(gc.getStatistics()); - } else { - return new Properties(); } + return new Properties(); } catch (IOException e) { throw new JGitInternalException( JGitText.get().couldNotGetRepoStatistics, e); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java index 66de8ae131..217785d39e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/LogCommand.java @@ -105,6 +105,7 @@ public class LogCommand extends GitCommand<Iterable<RevCommit>> { private RevFilter revFilter; private final List<PathFilter> pathFilters = new ArrayList<>(); + private final List<TreeFilter> excludeTreeFilters = new ArrayList<>(); private int maxCount = -1; @@ -133,9 +134,22 @@ public class LogCommand extends GitCommand<Iterable<RevCommit>> { @Override public Iterable<RevCommit> call() throws GitAPIException, NoHeadException { checkCallable(); - if (!pathFilters.isEmpty()) - walk.setTreeFilter(AndTreeFilter.create( - PathFilterGroup.create(pathFilters), TreeFilter.ANY_DIFF)); + List<TreeFilter> filters = new ArrayList<>(); + if (!pathFilters.isEmpty()) { + filters.add(AndTreeFilter.create(PathFilterGroup.create(pathFilters), TreeFilter.ANY_DIFF)); + } + if (!excludeTreeFilters.isEmpty()) { + for (TreeFilter f : excludeTreeFilters) { + filters.add(AndTreeFilter.create(f, TreeFilter.ANY_DIFF)); + } + } + if (!filters.isEmpty()) { + if (filters.size() == 1) { + filters.add(TreeFilter.ANY_DIFF); + } + walk.setTreeFilter(AndTreeFilter.create(filters)); + + } if (skip > -1 && maxCount > -1) walk.setRevFilter(AndRevFilter.create(SkipRevFilter.create(skip), MaxCountRevFilter.create(maxCount))); @@ -310,6 +324,24 @@ public class LogCommand extends GitCommand<Iterable<RevCommit>> { } /** + * Show all commits that are not within any of the specified paths. The path + * must either name a file or a directory exactly and use <code>/</code> + * (slash) as separator. Note that regular expressions or wildcards are not + * yet supported. If a path is both added and excluded from the search, then + * the exclusion wins. + * + * @param path + * a repository-relative path (with <code>/</code> as separator) + * @return {@code this} + * @since 5.6 + */ + public LogCommand excludePath(String path) { + checkCallable(); + excludeTreeFilters.add(PathFilter.create(path).negate()); + return this; + } + + /** * Skip the number of commits before starting to show the commit output. * * @param skip diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java index f9a9baf919..9a843f63a0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java @@ -409,27 +409,24 @@ public class MergeCommand extends GitCommand<MergeResult> { new ObjectId[] { headCommit.getId(), srcCommit.getId() }, mergeStatus, mergeStrategy, null, msg); - } else { - if (failingPaths != null) { - repo.writeMergeCommitMsg(null); - repo.writeMergeHeads(null); - return new MergeResult(null, merger.getBaseCommitId(), - new ObjectId[] { - headCommit.getId(), srcCommit.getId() }, - MergeStatus.FAILED, mergeStrategy, - lowLevelResults, failingPaths, null); - } else { - String mergeMessageWithConflicts = new MergeMessageFormatter() - .formatWithConflicts(mergeMessage, - unmergedPaths); - repo.writeMergeCommitMsg(mergeMessageWithConflicts); - return new MergeResult(null, merger.getBaseCommitId(), - new ObjectId[] { headCommit.getId(), - srcCommit.getId() }, - MergeStatus.CONFLICTING, mergeStrategy, - lowLevelResults, null); - } } + if (failingPaths != null) { + repo.writeMergeCommitMsg(null); + repo.writeMergeHeads(null); + return new MergeResult(null, merger.getBaseCommitId(), + new ObjectId[] { headCommit.getId(), + srcCommit.getId() }, + MergeStatus.FAILED, mergeStrategy, lowLevelResults, + failingPaths, null); + } + String mergeMessageWithConflicts = new MergeMessageFormatter() + .formatWithConflicts(mergeMessage, unmergedPaths); + repo.writeMergeCommitMsg(mergeMessageWithConflicts); + return new MergeResult(null, merger.getBaseCommitId(), + new ObjectId[] { headCommit.getId(), + srcCommit.getId() }, + MergeStatus.CONFLICTING, mergeStrategy, lowLevelResults, + null); } } catch (org.eclipse.jgit.errors.CheckoutConflictException e) { List<String> conflicts = (dco == null) ? Collections diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java index bdb2d1bbc5..ea3e2d9f83 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java @@ -90,7 +90,7 @@ import org.eclipse.jgit.transport.TagOpt; */ public class PullCommand extends TransportCommand<PullCommand, PullResult> { - private final static String DOT = "."; //$NON-NLS-1$ + private static final String DOT = "."; //$NON-NLS-1$ private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; @@ -315,23 +315,24 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> { Ref r = null; if (fetchRes != null) { r = fetchRes.getAdvertisedRef(remoteBranchName); - if (r == null) + if (r == null) { r = fetchRes.getAdvertisedRef(Constants.R_HEADS + remoteBranchName); + } } if (r == null) { throw new RefNotAdvertisedException(MessageFormat.format( JGitText.get().couldNotGetAdvertisedRef, remote, remoteBranchName)); - } else { - commitToMerge = r.getObjectId(); } + commitToMerge = r.getObjectId(); } else { try { commitToMerge = repo.resolve(remoteBranchName); - if (commitToMerge == null) + if (commitToMerge == null) { throw new RefNotFoundException(MessageFormat.format( JGitText.get().refNotResolved, remoteBranchName)); + } } catch (IOException e) { throw new JGitInternalException( JGitText.get().exceptionCaughtDuringExecutionOfPullCommand, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java index 0dacd4dfbf..6eed0b3f64 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -509,10 +509,10 @@ public class RebaseCommand extends GitCommand<RebaseResult> { monitor.beginTask(MessageFormat.format( JGitText.get().applyingCommit, commitToPick.getShortMessage()), ProgressMonitor.UNKNOWN); - if (preserveMerges) + if (preserveMerges) { return cherryPickCommitPreservingMerges(commitToPick); - else - return cherryPickCommitFlattening(commitToPick); + } + return cherryPickCommitFlattening(commitToPick); } finally { monitor.endTask(); } @@ -539,11 +539,11 @@ public class RebaseCommand extends GitCommand<RebaseResult> { .call(); switch (cherryPickResult.getStatus()) { case FAILED: - if (operation == Operation.BEGIN) + if (operation == Operation.BEGIN) { return abort(RebaseResult .failed(cherryPickResult.getFailingPaths())); - else - return stop(commitToPick, Status.STOPPED); + } + return stop(commitToPick, Status.STOPPED); case CONFLICTING: return stop(commitToPick, Status.STOPPED); case OK: @@ -599,11 +599,11 @@ public class RebaseCommand extends GitCommand<RebaseResult> { CherryPickResult cherryPickResult = pickCommand.call(); switch (cherryPickResult.getStatus()) { case FAILED: - if (operation == Operation.BEGIN) + if (operation == Operation.BEGIN) { return abort(RebaseResult.failed( cherryPickResult.getFailingPaths())); - else - return stop(commitToPick, Status.STOPPED); + } + return stop(commitToPick, Status.STOPPED); case CONFLICTING: return stop(commitToPick, Status.STOPPED); case OK: @@ -833,7 +833,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> { sb.append("# This is a combination of ").append(count) .append(" commits.\n"); // Add the previous message without header (i.e first line) - sb.append(currSquashMessage.substring(currSquashMessage.indexOf("\n") + 1)); + sb.append(currSquashMessage + .substring(currSquashMessage.indexOf('\n') + 1)); sb.append("\n"); if (isSquash) { sb.append("# This is the ").append(count).append(ordinal) @@ -871,7 +872,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { static int parseSquashFixupSequenceCount(String currSquashMessage) { String regex = "This is a combination of (.*) commits"; //$NON-NLS-1$ String firstLine = currSquashMessage.substring(0, - currSquashMessage.indexOf("\n")); //$NON-NLS-1$ + currSquashMessage.indexOf('\n')); Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(firstLine); if (!matcher.find()) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java index d7c9ad5e04..3031a197a8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java @@ -334,10 +334,10 @@ public class ResetCommand extends GitCommand<Ref> { } private String getRefOrHEAD() { - if (ref != null) + if (ref != null) { return ref; - else - return Constants.HEAD; + } + return Constants.HEAD; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java index ddd60b6fa2..aa0055fb53 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java @@ -58,6 +58,7 @@ import org.eclipse.jgit.api.errors.NoMessageException; import org.eclipse.jgit.api.errors.UnmergedPathsException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.events.WorkingTreeModifiedEvent; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; @@ -175,6 +176,10 @@ public class RevertCommand extends GitCommand<RevCommit> { + "This reverts commit " + srcCommit.getId().getName() //$NON-NLS-1$ + ".\n"; //$NON-NLS-1$ if (merger.merge(headCommit, srcParent)) { + if (!merger.getModifiedFiles().isEmpty()) { + repo.fireEvent(new WorkingTreeModifiedEvent( + merger.getModifiedFiles(), null)); + } if (AnyObjectId.isEqual(headCommit.getTree().getId(), merger.getResultTreeId())) continue; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java index 52393695d9..74def9e897 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java @@ -106,10 +106,10 @@ public class SubmoduleSyncCommand extends GitCommand<Map<String, String>> { */ protected String getHeadBranch(Repository subRepo) throws IOException { Ref head = subRepo.exactRef(Constants.HEAD); - if (head != null && head.isSymbolic()) + if (head != null && head.isSymbolic()) { return Repository.shortenRefName(head.getLeaf().getName()); - else - return null; + } + return null; } /** {@inheritDoc} */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/AbortedByHookException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/AbortedByHookException.java index db6440b55f..30a2d622a7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/AbortedByHookException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/AbortedByHookException.java @@ -67,19 +67,26 @@ public class AbortedByHookException extends GitAPIException { private final int returnCode; /** + * The stderr output of the hook. + */ + private final String hookStdErr; + + /** * Constructor for AbortedByHookException * - * @param message - * The error details. + * @param hookStdErr + * The error details from the stderr output of the hook * @param hookName * The name of the hook that interrupted the command, must not be * null. * @param returnCode * The return code of the hook process that has been run. */ - public AbortedByHookException(String message, String hookName, + public AbortedByHookException(String hookStdErr, String hookName, int returnCode) { - super(message); + super(MessageFormat.format(JGitText.get().commandRejectedByHook, + hookName, hookStdErr)); + this.hookStdErr = hookStdErr; this.hookName = hookName; this.returnCode = returnCode; } @@ -102,10 +109,13 @@ public class AbortedByHookException extends GitAPIException { return returnCode; } - /** {@inheritDoc} */ - @Override - public String getMessage() { - return MessageFormat.format(JGitText.get().commandRejectedByHook, - hookName, super.getMessage()); + /** + * Get the stderr output of the hook. + * + * @return A string containing the complete stderr output of the hook. + * @since 5.6 + */ + public String getHookStdErr() { + return hookStdErr; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java index f1df0da453..b732050bbb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java @@ -67,7 +67,7 @@ public final class Attribute { * The attribute value state * see also https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html */ - public static enum State { + public enum State { /** the attribute is set */ SET, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java index a91d8c282f..ead99c79c5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java @@ -91,7 +91,7 @@ public class AttributesRule { continue; } - final int equalsIndex = attribute.indexOf("="); //$NON-NLS-1$ + final int equalsIndex = attribute.indexOf('='); if (equalsIndex == -1) result.add(new Attribute(attribute, State.SET)); else { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java index 9cec645679..d0aa292df4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, Google Inc. + * Copyright (C) 2011, 2019 Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -48,11 +48,17 @@ import static org.eclipse.jgit.lib.FileMode.TYPE_FILE; import static org.eclipse.jgit.lib.FileMode.TYPE_MASK; import java.io.IOException; +import java.io.InputStream; +import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.blame.Candidate.BlobCandidate; +import org.eclipse.jgit.blame.Candidate.HeadCandidate; import org.eclipse.jgit.blame.Candidate.ReverseCandidate; import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit; import org.eclipse.jgit.diff.DiffAlgorithm; @@ -63,8 +69,13 @@ import org.eclipse.jgit.diff.HistogramDiff; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.diff.RenameDetector; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -74,9 +85,12 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.TreeWalk.OperationType; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; +import org.eclipse.jgit.util.IO; /** * Generate author information for lines based on a provided file. @@ -313,6 +327,107 @@ public class BlameGenerator implements AutoCloseable { } /** + * Pushes HEAD, index, and working tree as appropriate for blaming the file + * given in the constructor {@link #BlameGenerator(Repository, String)} + * against HEAD. Includes special handling in case the file is in conflict + * state from an unresolved merge conflict. + * + * @return {@code this} + * @throws NoHeadException + * if the repository has no HEAD + * @throws IOException + * if an error occurs + * @since 5.6 + */ + public BlameGenerator prepareHead() throws NoHeadException, IOException { + Repository repo = getRepository(); + ObjectId head = repo.resolve(Constants.HEAD); + if (head == null) { + throw new NoHeadException(MessageFormat + .format(JGitText.get().noSuchRefKnown, Constants.HEAD)); + } + if (repo.isBare()) { + return push(null, head); + } + DirCache dc = repo.readDirCache(); + try (TreeWalk walk = new TreeWalk(repo)) { + walk.setOperationType(OperationType.CHECKIN_OP); + FileTreeIterator iter = new FileTreeIterator(repo); + int fileTree = walk.addTree(iter); + int indexTree = walk.addTree(new DirCacheIterator(dc)); + iter.setDirCacheIterator(walk, indexTree); + walk.setFilter(resultPath); + walk.setRecursive(true); + if (!walk.next()) { + return this; + } + DirCacheIterator dcIter = walk.getTree(indexTree, + DirCacheIterator.class); + if (dcIter == null) { + // Not found in index + return this; + } + iter = walk.getTree(fileTree, FileTreeIterator.class); + if (iter == null || !isFile(iter.getEntryRawMode())) { + return this; + } + RawText inTree; + long filteredLength = iter.getEntryContentLength(); + try (InputStream stream = iter.openEntryStream()) { + inTree = new RawText(getBytes(iter.getEntryFile().getPath(), + stream, filteredLength)); + } + DirCacheEntry indexEntry = dcIter.getDirCacheEntry(); + if (indexEntry.getStage() == DirCacheEntry.STAGE_0) { + push(null, head); + push(null, indexEntry.getObjectId()); + push(null, inTree); + } else { + // Create a special candidate using the working tree file as + // blob and HEAD and the MERGE_HEADs as parents. + HeadCandidate c = new HeadCandidate(getRepository(), resultPath, + getHeads(repo, head)); + c.sourceText = inTree; + c.regionList = new Region(0, 0, inTree.size()); + remaining = inTree.size(); + push(c); + } + } + return this; + } + + private List<RevCommit> getHeads(Repository repo, ObjectId head) + throws NoWorkTreeException, IOException { + List<ObjectId> mergeIds = repo.readMergeHeads(); + if (mergeIds == null || mergeIds.isEmpty()) { + return Collections.singletonList(revPool.parseCommit(head)); + } + List<RevCommit> heads = new ArrayList<>(mergeIds.size() + 1); + heads.add(revPool.parseCommit(head)); + for (ObjectId id : mergeIds) { + heads.add(revPool.parseCommit(id)); + } + return heads; + } + + private static byte[] getBytes(String path, InputStream in, long maxLength) + throws IOException { + if (maxLength > Integer.MAX_VALUE) { + throw new IOException( + MessageFormat.format(JGitText.get().fileIsTooLarge, path)); + } + int max = (int) maxLength; + byte[] buffer = new byte[max]; + int read = IO.readFully(in, buffer, 0); + if (read == max) { + return buffer; + } + byte[] copy = new byte[read]; + System.arraycopy(buffer, 0, copy, 0, read); + return copy; + } + + /** * Push a candidate object onto the generator's traversal stack. * <p> * Candidates should be pushed in history order from oldest-to-newest. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java index 5fb77501fa..394aba6a59 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameResult.java @@ -267,18 +267,18 @@ public class BlameResult { */ public int computeNext() throws IOException { BlameGenerator gen = generator; - if (gen == null) + if (gen == null) { return -1; + } if (gen.next()) { loadFrom(gen); lastLength = gen.getRegionLength(); return gen.getResultStart(); - } else { - gen.close(); - generator = null; - return -1; } + gen.close(); + generator = null; + return -1; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java index 457d1d2cea..3ef4943982 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/Candidate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, Google Inc. + * Copyright (C) 2011, 2019 Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -44,12 +44,14 @@ package org.eclipse.jgit.blame; import java.io.IOException; +import java.util.List; import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit; import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.EditList; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -392,6 +394,66 @@ class Candidate { } /** + * A {@link Candidate} to blame a working tree file in conflict state. + * <p> + * Contrary to {@link BlobCandidate}, it expects to be given the parent + * commits (typically HEAD and the MERGE_HEADs) and behaves like a merge + * commit during blame. It does <em>not</em> consider a previously pushed + * Candidate as its parent. + * </p> + */ + static final class HeadCandidate extends Candidate { + + private List<RevCommit> parents; + + HeadCandidate(Repository repo, PathFilter path, + List<RevCommit> parents) { + super(repo, null, path); + this.parents = parents; + } + + @Override + void beginResult(RevWalk rw) { + // Blob candidates have nothing to prepare. + } + + @Override + int getParentCount() { + return parents.size(); + } + + @Override + RevCommit getParent(int idx) { + return parents.get(idx); + } + + @Override + boolean has(RevFlag flag) { + return true; // Pretend flag was added; sourceCommit is null. + } + + @Override + void add(RevFlag flag) { + // Do nothing, sourceCommit is null. + } + + @Override + void remove(RevFlag flag) { + // Do nothing, sourceCommit is null. + } + + @Override + int getTime() { + return Integer.MAX_VALUE; + } + + @Override + PersonIdent getAuthor() { + return new PersonIdent(JGitText.get().blameNotCommittedYet, ""); //$NON-NLS-1$ + } + } + + /** * Candidate loaded from a file source, and not a commit. * <p> * The {@link Candidate#sourceCommit} field is always null on this type of diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffConfig.java index ca37a10c5a..9071aeb09c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffConfig.java @@ -59,7 +59,7 @@ public class DiffConfig { public static final Config.SectionParser<DiffConfig> KEY = DiffConfig::new; /** Permissible values for {@code diff.renames}. */ - public static enum RenameDetectionType { + public enum RenameDetectionType { /** Rename detection is disabled. */ FALSE, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java index 5c8343f92c..9f660ec1c8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java @@ -72,7 +72,7 @@ public class DiffEntry { public static final String DEV_NULL = "/dev/null"; //$NON-NLS-1$ /** General type of change a single file-level patch describes. */ - public static enum ChangeType { + public enum ChangeType { /** Add a new file to the project */ ADD, @@ -90,7 +90,7 @@ public class DiffEntry { } /** Specify the old or new side for more generalized access. */ - public static enum Side { + public enum Side { /** The old side of a DiffEntry. */ OLD, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java index 1cecff6fb0..d764499159 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java @@ -145,6 +145,8 @@ public class DiffFormatter implements AutoCloseable { private Repository repository; + private Boolean quotePaths; + /** * Create a new formatter with a default level of context. * @@ -199,6 +201,11 @@ public class DiffFormatter implements AutoCloseable { this.closeReader = closeReader; this.reader = reader; this.diffCfg = cfg.get(DiffConfig.KEY); + if (quotePaths == null) { + quotePaths = Boolean + .valueOf(cfg.getBoolean(ConfigConstants.CONFIG_CORE_SECTION, + ConfigConstants.CONFIG_KEY_QUOTE_PATH, true)); + } ContentSource cs = ContentSource.create(reader); source = new ContentSource.Pair(cs, cs); @@ -379,6 +386,21 @@ public class DiffFormatter implements AutoCloseable { } /** + * Sets whether or not path names should be quoted. + * <p> + * By default the setting of git config {@code core.quotePath} is active, + * but this can be overridden through this method. + * </p> + * + * @param quote + * whether to quote path names + * @since 5.6 + */ + public void setQuotePaths(boolean quote) { + quotePaths = Boolean.valueOf(quote); + } + + /** * Set the filter to produce only specific paths. * * If the filter is an instance of @@ -489,8 +511,8 @@ public class DiffFormatter implements AutoCloseable { CanonicalTreeParser parser = new CanonicalTreeParser(); parser.reset(reader, tree); return parser; - } else - return new EmptyTreeIterator(); + } + return new EmptyTreeIterator(); } /** @@ -726,8 +748,11 @@ public class DiffFormatter implements AutoCloseable { return id.name(); } - private static String quotePath(String name) { - return QuotedString.GIT_PATH.quote(name); + private String quotePath(String path) { + if (quotePaths == null || quotePaths.booleanValue()) { + return QuotedString.GIT_PATH.quote(path); + } + return QuotedString.GIT_PATH_MINIMAL.quote(path); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java index 5c876e87fd..b4e09636c7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/Edit.java @@ -64,7 +64,7 @@ package org.eclipse.jgit.diff; */ public class Edit { /** Type of edit */ - public static enum Type { + public enum Type { /** Sequence B has inserted the region. */ INSERT, @@ -125,17 +125,17 @@ public class Edit { */ public final Type getType() { if (beginA < endA) { - if (beginB < endB) + if (beginB < endB) { return Type.REPLACE; - else /* if (beginB == endB) */ - return Type.DELETE; - - } else /* if (beginA == endA) */{ - if (beginB < endB) - return Type.INSERT; - else /* if (beginB == endB) */ - return Type.EMPTY; + } + return Type.DELETE; + + } + if (beginB < endB) { + return Type.INSERT; } + // beginB == endB) + return Type.EMPTY; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java index 6c0d90ebad..4e79fc9386 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RawText.java @@ -383,15 +383,17 @@ public class RawText extends Sequence { * @return the line delimiter or <code>null</code> */ public String getLineDelimiter() { - if (size() == 0) + if (size() == 0) { return null; + } int e = getEnd(0); - if (content[e - 1] != '\n') + if (content[e - 1] != '\n') { return null; - if (content.length > 1 && e > 1 && content[e - 2] == '\r') + } + if (content.length > 1 && e > 1 && content[e - 2] == '\r') { return "\r\n"; //$NON-NLS-1$ - else - return "\n"; //$NON-NLS-1$ + } + return "\n"; //$NON-NLS-1$ } /** @@ -446,7 +448,7 @@ public class RawText extends Sequence { } } - byte data[]; + byte[] data; try { data = new byte[(int)sz]; } catch (OutOfMemoryError e) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java index d8a05c34ea..1b7babcecb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java @@ -335,14 +335,14 @@ class SimilarityRenameDetector { } static int nameScore(String a, String b) { - int aDirLen = a.lastIndexOf("/") + 1; //$NON-NLS-1$ - int bDirLen = b.lastIndexOf("/") + 1; //$NON-NLS-1$ + int aDirLen = a.lastIndexOf('/') + 1; + int bDirLen = b.lastIndexOf('/') + 1; - int dirMin = Math.min(aDirLen, bDirLen); - int dirMax = Math.max(aDirLen, bDirLen); + int dirMin = Math.min(aDirLen, bDirLen); + int dirMax = Math.max(aDirLen, bDirLen); - final int dirScoreLtr; - final int dirScoreRtl; + final int dirScoreLtr; + final int dirScoreRtl; if (dirMax == 0) { dirScoreLtr = 100; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java index 6bc2946078..3502d7a43a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -53,9 +53,11 @@ import java.text.MessageFormat; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.FilterFailedException; @@ -104,7 +106,8 @@ import org.slf4j.LoggerFactory; * This class handles checking out one or two trees merging with the index. */ public class DirCacheCheckout { - private static Logger LOG = LoggerFactory.getLogger(DirCacheCheckout.class); + private static final Logger LOG = LoggerFactory + .getLogger(DirCacheCheckout.class); private static final int MAX_EXCEPTION_TEXT_SIZE = 10 * 1024; @@ -142,6 +145,8 @@ public class DirCacheCheckout { private ArrayList<String> removed = new ArrayList<>(); + private ArrayList<String> kept = new ArrayList<>(); + private ObjectId mergeCommitTree; private DirCache dc; @@ -432,11 +437,11 @@ public class DirCacheCheckout { if (mtime == null || mtime.equals(Instant.EPOCH)) { entry.setLastModified(f.getEntryLastModifiedInstant()); } - keep(entry, f); + keep(i.getEntryPathString(), entry, f); } } else // The index contains a folder - keep(i.getDirCacheEntry(), f); + keep(i.getEntryPathString(), i.getDirCacheEntry(), f); } else { // There is no entry in the merge commit. Means: we want to delete // what's currently in the index and working tree @@ -496,8 +501,11 @@ public class DirCacheCheckout { dc.unlock(); } finally { if (performingCheckout) { + Set<String> touched = new HashSet<>(conflicts); + touched.addAll(getUpdated().keySet()); + touched.addAll(kept); WorkingTreeModifiedEvent event = new WorkingTreeModifiedEvent( - getUpdated().keySet(), getRemoved()); + touched, getRemoved()); if (!event.isEmpty()) { repo.fireEvent(event); } @@ -517,10 +525,10 @@ public class DirCacheCheckout { prescanOneTree(); if (!conflicts.isEmpty()) { - if (failOnConflict) + if (failOnConflict) { throw new CheckoutConflictException(conflicts.toArray(new String[0])); - else - cleanUpConflicts(); + } + cleanUpConflicts(); } // update our index @@ -826,14 +834,14 @@ public class DirCacheCheckout { break; case 0xDFD: // 3 4 - keep(dce, f); + keep(name, dce, f); break; case 0xF0D: // 18 remove(name); break; case 0xDFF: // 5 5b 6 6b if (equalIdAndMode(iId, iMode, mId, mMode)) - keep(dce, f); // 5 6 + keep(name, dce, f); // 5 6 else conflict(name, dce, h, m); // 5b 6b break; @@ -863,7 +871,7 @@ public class DirCacheCheckout { conflict(name, dce, h, m); // 9 break; case 0xFD0: // keep without a rule - keep(dce, f); + keep(name, dce, f); break; case 0xFFD: // 12 13 14 if (equalIdAndMode(hId, hMode, iId, iMode)) @@ -883,7 +891,7 @@ public class DirCacheCheckout { conflict(name, dce, h, m); break; default: - keep(dce, f); + keep(name, dce, f); } return; } @@ -895,15 +903,14 @@ public class DirCacheCheckout { // the workingtree entry doesn't exist or also contains a folder // -> no problem return; - } else { - // the workingtree entry exists and is not a folder - if (!idEqual(h, m)) { - // Because HEAD and MERGE differ we will try to update the - // workingtree with a folder -> return a conflict - conflict(name, null, null, null); - } - return; } + // the workingtree entry exists and is not a folder + if (!idEqual(h, m)) { + // Because HEAD and MERGE differ we will try to update the + // workingtree with a folder -> return a conflict + conflict(name, null, null, null); + } + return; } if ((ffMask == 0x00F) && f != null && FileMode.TREE.equals(f.getEntryFileMode())) { @@ -969,7 +976,7 @@ public class DirCacheCheckout { if (initialCheckout) update(name, mId, mMode); else - keep(dce, f); + keep(name, dce, f); } else conflict(name, dce, h, m); } @@ -1032,7 +1039,7 @@ public class DirCacheCheckout { // Nothing in Head // Something in Index // -> Merge contains nothing new. Keep the index. - keep(dce, f); + keep(name, dce, f); } else // Merge contains something and it is not the same as Index // Nothing in Head @@ -1083,15 +1090,15 @@ public class DirCacheCheckout { // Something in Head if (!FileMode.TREE.equals(f.getEntryFileMode()) - && FileMode.TREE.equals(iMode)) + && FileMode.TREE.equals(iMode)) { // The workingtree contains a file and the index semantically contains a folder. // Git considers the workingtree file as untracked. Just keep the untracked file. return; - else - // -> file is dirty and tracked but is should be - // removed. That's a conflict - conflict(name, dce, h, m); - } else + } + // -> file is dirty and tracked but is should be + // removed. That's a conflict + conflict(name, dce, h, m); + } else { // file doesn't exist or is clean // Index contains the same as Head // Something different from a submodule in Index @@ -1099,7 +1106,8 @@ public class DirCacheCheckout { // Something in Head // -> Remove from index and delete the file remove(name); - } else + } + } else { // Index contains something different from Head // Something different from a submodule in Index // Nothing in Merge @@ -1108,6 +1116,7 @@ public class DirCacheCheckout { // filesystem). But Merge wants the path to be removed. // Report a conflict conflict(name, dce, h, m); + } } } else { // Something in Merge @@ -1181,7 +1190,7 @@ public class DirCacheCheckout { // to the other one. // -> In all three cases we don't touch index and file. - keep(dce, f); + keep(name, dce, f); } } } @@ -1230,13 +1239,17 @@ public class DirCacheCheckout { } } - private void keep(DirCacheEntry e, WorkingTreeIterator f) + private void keep(String path, DirCacheEntry e, WorkingTreeIterator f) throws IOException { if (e != null && !FileMode.TREE.equals(e.getFileMode())) builder.add(e); if (force) { - if (f.isModified(e, true, this.walk.getObjectReader())) { - checkoutEntry(repo, e, this.walk.getObjectReader()); + if (f.isModified(e, true, walk.getObjectReader())) { + kept.add(path); + checkoutEntry(repo, e, walk.getObjectReader(), false, + new CheckoutMetadata(walk.getEolStreamType(CHECKOUT_OP), + walk.getFilterCommand( + Constants.ATTR_FILTER_TYPE_SMUDGE))); } } } @@ -1340,13 +1353,14 @@ public class DirCacheCheckout { private boolean isModified_IndexTree(String path, ObjectId iId, FileMode iMode, ObjectId tId, FileMode tMode, ObjectId rootTree) throws CorruptObjectException, IOException { - if (iMode != tMode) + if (iMode != tMode) { return true; + } if (FileMode.TREE.equals(iMode) - && (iId == null || ObjectId.zeroId().equals(iId))) + && (iId == null || ObjectId.zeroId().equals(iId))) { return isModifiedSubtree_IndexTree(path, rootTree); - else - return !equalIdAndMode(iId, iMode, tId, tMode); + } + return !equalIdAndMode(iId, iMode, tId, tMode); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java index 0e91f0d748..cbf96e468c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java @@ -824,10 +824,10 @@ public class DirCacheEntry { } private int getExtendedFlags() { - if (isExtended()) + if (isExtended()) { return NB.decodeUInt16(info, infoOffset + P_FLAGS2) << 16; - else - return 0; + } + return 0; } private static void checkPath(byte[] path) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoMergeBaseException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoMergeBaseException.java index 97214b0ede..023dfe9657 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoMergeBaseException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoMergeBaseException.java @@ -63,7 +63,7 @@ public class NoMergeBaseException extends IOException { * An enum listing the different reason why no merge base could be * determined. */ - public static enum MergeBaseFailureReason { + public enum MergeBaseFailureReason { /** * Multiple merge bases have been found (e.g. the commits to be merged * have multiple common predecessors) but the merge strategy doesn't diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/TranslationBundleException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/TranslationBundleException.java index 70760f36bf..6d18c7c512 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/TranslationBundleException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/TranslationBundleException.java @@ -78,7 +78,7 @@ public abstract class TranslationBundleException extends RuntimeException { * * @return bundle class for which the exception occurred */ - final public Class getBundleClass() { + public final Class getBundleClass() { return bundleClass; } @@ -87,7 +87,7 @@ public abstract class TranslationBundleException extends RuntimeException { * * @return locale for which the exception occurred */ - final public Locale getLocale() { + public final Locale getLocale() { return locale; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java index 60669bb955..2c93d55f4d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/AbstractHead.java @@ -82,10 +82,10 @@ abstract class AbstractHead implements Head { /** {@inheritDoc} */ @Override public List<Head> getNextHeads(char c) { - if (matches(c)) + if (matches(c)) { return newHeads; - else - return FileNameMatcher.EMPTY_HEAD_LIST; + } + return FileNameMatcher.EMPTY_HEAD_LIST; } boolean isStar() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/FileNameMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/FileNameMatcher.java index bfcc580ba4..8125c356a6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/FileNameMatcher.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/fnmatch/FileNameMatcher.java @@ -304,11 +304,11 @@ public class FileNameMatcher { private static AbstractHead createWildCardHead( final Character invalidWildgetCharacter, final boolean star) { - if (invalidWildgetCharacter != null) + if (invalidWildgetCharacter != null) { return new RestrictedWildCardHead(invalidWildgetCharacter .charValue(), star); - else - return new WildCardHead(star); + } + return new WildCardHead(star); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java index 8e463415b8..febdb92091 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java @@ -293,13 +293,12 @@ public class ManifestParser extends DefaultHandler { String revision = defaultRevision; if (remote == null) { if (defaultRemote == null) { - if (filename != null) + if (filename != null) { throw new SAXException(MessageFormat.format( RepoText.get().errorNoDefaultFilename, filename)); - else - throw new SAXException( - RepoText.get().errorNoDefault); + } + throw new SAXException(RepoText.get().errorNoDefault); } remote = defaultRemote; } else { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java index cb62925a1f..7288678007 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java @@ -761,18 +761,17 @@ public class RepoCommand extends GitCommand<RevCommit> { } catch (GitAPIException | IOException e) { throw new ManifestErrorException(e); } - } else { - try (Git git = new Git(repo)) { - for (RepoProject proj : filteredProjects) { - addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(), - proj.getRevision(), proj.getCopyFiles(), - proj.getLinkFiles(), git); - } - return git.commit().setMessage(RepoText.get().repoCommitMessage) - .call(); - } catch (GitAPIException | IOException e) { - throw new ManifestErrorException(e); + } + try (Git git = new Git(repo)) { + for (RepoProject proj : filteredProjects) { + addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(), + proj.getRevision(), proj.getCopyFiles(), + proj.getLinkFiles(), git); } + return git.commit().setMessage(RepoText.get().repoCommitMessage) + .call(); + } catch (GitAPIException | IOException e) { + throw new ManifestErrorException(e); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java index d79dfa8b2f..684d1e457f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java @@ -422,10 +422,10 @@ public class RepoProject implements Comparable<RepoProject> { } private String getPathWithSlash() { - if (path.endsWith("/")) //$NON-NLS-1$ + if (path.endsWith("/")) { //$NON-NLS-1$ return path; - else - return path + "/"; //$NON-NLS-1$ + } + return path + "/"; //$NON-NLS-1$ } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java index f33168d814..6dbe0a6609 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/CommitMsgHook.java @@ -72,6 +72,9 @@ public class CommitMsgHook extends GitHook<String> { /** * Constructor for CommitMsgHook + * <p> + * This constructor will use the default error stream. + * </p> * * @param repo * The repository @@ -83,6 +86,24 @@ public class CommitMsgHook extends GitHook<String> { super(repo, outputStream); } + /** + * Constructor for CommitMsgHook + * + * @param repo + * The repository + * @param outputStream + * The output stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.out}. + * @param errorStream + * The error stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.err}. + * @since 5.6 + */ + protected CommitMsgHook(Repository repo, PrintStream outputStream, + PrintStream errorStream) { + super(repo, outputStream, errorStream); + } + /** {@inheritDoc} */ @Override public String call() throws IOException, AbortedByHookException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java index ad43e2ca83..aa307c9378 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java @@ -50,6 +50,7 @@ import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.util.concurrent.Callable; +import org.bouncycastle.util.io.TeeOutputStream; import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.util.FS; @@ -79,7 +80,15 @@ abstract class GitHook<T> implements Callable<T> { protected final PrintStream outputStream; /** - * Constructor for GitHook + * The error stream to be used by the hook. + */ + protected final PrintStream errorStream; + + /** + * Constructor for GitHook. + * <p> + * This constructor will use stderr for the error stream. + * </p> * * @param repo * a {@link org.eclipse.jgit.lib.Repository} object. @@ -88,8 +97,26 @@ abstract class GitHook<T> implements Callable<T> { * in which case the hook will use {@code System.out}. */ protected GitHook(Repository repo, PrintStream outputStream) { + this(repo, outputStream, null); + } + + /** + * Constructor for GitHook + * + * @param repo + * a {@link org.eclipse.jgit.lib.Repository} object. + * @param outputStream + * The output stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.out}. + * @param errorStream + * The error stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.err}. + */ + protected GitHook(Repository repo, PrintStream outputStream, + PrintStream errorStream) { this.repo = repo; this.outputStream = outputStream; + this.errorStream = errorStream; } /** @@ -148,6 +175,16 @@ abstract class GitHook<T> implements Callable<T> { } /** + * Get error stream + * + * @return The error stream the hook must use. Never {@code null}, + * {@code System.err} is returned by default. + */ + protected PrintStream getErrorStream() { + return errorStream == null ? System.err : errorStream; + } + + /** * Runs the hook, without performing any validity checks. * * @throws org.eclipse.jgit.api.errors.AbortedByHookException @@ -155,16 +192,23 @@ abstract class GitHook<T> implements Callable<T> { */ protected void doRun() throws AbortedByHookException { final ByteArrayOutputStream errorByteArray = new ByteArrayOutputStream(); + final TeeOutputStream stderrStream = new TeeOutputStream(errorByteArray, + getErrorStream()); PrintStream hookErrRedirect = null; try { - hookErrRedirect = new PrintStream(errorByteArray, false, + hookErrRedirect = new PrintStream(stderrStream, false, UTF_8.name()); } catch (UnsupportedEncodingException e) { // UTF-8 is guaranteed to be available } - ProcessResult result = FS.DETECTED.runHookIfPresent(getRepository(), - getHookName(), getParameters(), getOutputStream(), - hookErrRedirect, getStdinArgs()); + Repository repository = getRepository(); + FS fs = repository.getFS(); + if (fs == null) { + fs = FS.DETECTED; + } + ProcessResult result = fs.runHookIfPresent(repository, getHookName(), + getParameters(), getOutputStream(), hookErrRedirect, + getStdinArgs()); if (result.isExecutedWithError()) { throw new AbortedByHookException( new String(errorByteArray.toByteArray(), UTF_8), @@ -180,7 +224,11 @@ abstract class GitHook<T> implements Callable<T> { * @since 4.11 */ public boolean isNativeHookPresent() { - return FS.DETECTED.findHook(getRepository(), getHookName()) != null; + FS fs = getRepository().getFS(); + if (fs == null) { + fs = FS.DETECTED; + } + return fs.findHook(getRepository(), getHookName()) != null; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java index b801d6872b..f29dcd178b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/Hooks.java @@ -57,7 +57,8 @@ import org.eclipse.jgit.util.LfsFactory; public class Hooks { /** - * Create pre-commit hook for the given repository + * Create pre-commit hook for the given repository with the default error + * stream * * @param repo * a {@link org.eclipse.jgit.lib.Repository} object. @@ -71,7 +72,25 @@ public class Hooks { } /** - * Create post-commit hook for the given repository + * Create pre-commit hook for the given repository + * + * @param repo + * a {@link org.eclipse.jgit.lib.Repository} object. + * @param outputStream + * The output stream, or {@code null} to use {@code System.out} + * @param errorStream + * The error stream, or {@code null} to use {@code System.err} + * @return The pre-commit hook for the given repository. + * @since 5.6 + */ + public static PreCommitHook preCommit(Repository repo, + PrintStream outputStream, PrintStream errorStream) { + return new PreCommitHook(repo, outputStream, errorStream); + } + + /** + * Create post-commit hook for the given repository with the default error + * stream * * @param repo * a {@link org.eclipse.jgit.lib.Repository} object. @@ -86,7 +105,25 @@ public class Hooks { } /** - * Create commit-msg hook for the given repository + * Create post-commit hook for the given repository + * + * @param repo + * a {@link org.eclipse.jgit.lib.Repository} object. + * @param outputStream + * The output stream, or {@code null} to use {@code System.out} + * @param errorStream + * The error stream, or {@code null} to use {@code System.err} + * @return The pre-commit hook for the given repository. + * @since 5.6 + */ + public static PostCommitHook postCommit(Repository repo, + PrintStream outputStream, PrintStream errorStream) { + return new PostCommitHook(repo, outputStream, errorStream); + } + + /** + * Create commit-msg hook for the given repository with the default error + * stream * * @param repo * a {@link org.eclipse.jgit.lib.Repository} object. @@ -100,7 +137,25 @@ public class Hooks { } /** - * Create pre-push hook for the given repository + * Create commit-msg hook for the given repository + * + * @param repo + * a {@link org.eclipse.jgit.lib.Repository} object. + * @param outputStream + * The output stream, or {@code null} to use {@code System.out} + * @param errorStream + * The error stream, or {@code null} to use {@code System.err} + * @return The pre-commit hook for the given repository. + * @since 5.6 + */ + public static CommitMsgHook commitMsg(Repository repo, + PrintStream outputStream, PrintStream errorStream) { + return new CommitMsgHook(repo, outputStream, errorStream); + } + + /** + * Create pre-push hook for the given repository with the default error + * stream * * @param repo * a {@link org.eclipse.jgit.lib.Repository} object. @@ -127,4 +182,36 @@ public class Hooks { } return new PrePushHook(repo, outputStream); } + + /** + * Create pre-push hook for the given repository + * + * @param repo + * a {@link org.eclipse.jgit.lib.Repository} object. + * @param outputStream + * The output stream, or {@code null} to use {@code System.out} + * @param errorStream + * The error stream, or {@code null} to use {@code System.err} + * @return The pre-push hook for the given repository. + * @since 5.6 + */ + public static PrePushHook prePush(Repository repo, PrintStream outputStream, + PrintStream errorStream) { + if (LfsFactory.getInstance().isAvailable()) { + PrePushHook hook = LfsFactory.getInstance().getPrePushHook(repo, + outputStream, errorStream); + if (hook != null) { + if (hook.isNativeHookPresent()) { + PrintStream ps = outputStream; + if (ps == null) { + ps = System.out; + } + ps.println(MessageFormat + .format(JGitText.get().lfsHookConflict, repo)); + } + return hook; + } + } + return new PrePushHook(repo, outputStream, errorStream); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java index 24bad16ecb..b6e576fc23 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PostCommitHook.java @@ -61,6 +61,9 @@ public class PostCommitHook extends GitHook<Void> { /** * Constructor for PostCommitHook + * <p> + * This constructor will use the default error stream. + * </p> * * @param repo * The repository @@ -72,6 +75,24 @@ public class PostCommitHook extends GitHook<Void> { super(repo, outputStream); } + /** + * Constructor for PostCommitHook + * + * @param repo + * The repository + * @param outputStream + * The output stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.out}. + * @param errorStream + * The error stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.err}. + * @since 5.6 + */ + protected PostCommitHook(Repository repo, PrintStream outputStream, + PrintStream errorStream) { + super(repo, outputStream, errorStream); + } + /** {@inheritDoc} */ @Override public Void call() throws IOException, AbortedByHookException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java index 0d9290da3e..dbdaf8669c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PreCommitHook.java @@ -61,6 +61,9 @@ public class PreCommitHook extends GitHook<Void> { /** * Constructor for PreCommitHook + * <p> + * This constructor will use the default error stream. + * </p> * * @param repo * The repository @@ -72,6 +75,24 @@ public class PreCommitHook extends GitHook<Void> { super(repo, outputStream); } + /** + * Constructor for PreCommitHook + * + * @param repo + * The repository + * @param outputStream + * The output stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.out}. + * @param errorStream + * The error stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.err}. + * @since 5.6 + */ + protected PreCommitHook(Repository repo, PrintStream outputStream, + PrintStream errorStream) { + super(repo, outputStream, errorStream); + } + /** {@inheritDoc} */ @Override public Void call() throws IOException, AbortedByHookException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java index 431944f9d4..61180fd021 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java @@ -73,6 +73,9 @@ public class PrePushHook extends GitHook<String> { /** * Constructor for PrePushHook + * <p> + * This constructor will use the default error stream. + * </p> * * @param repo * The repository @@ -84,6 +87,24 @@ public class PrePushHook extends GitHook<String> { super(repo, outputStream); } + /** + * Constructor for PrePushHook + * + * @param repo + * The repository + * @param outputStream + * The output stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.out}. + * @param errorStream + * The error stream the hook must use. {@code null} is allowed, + * in which case the hook will use {@code System.err}. + * @since 5.6 + */ + protected PrePushHook(Repository repo, PrintStream outputStream, + PrintStream errorStream) { + super(repo, outputStream, errorStream); + } + /** {@inheritDoc} */ @Override protected String getStdinArgs() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java index 31e173bf23..4bc926ae11 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java @@ -62,7 +62,7 @@ import org.slf4j.LoggerFactory; * @since 3.6 */ public class FastIgnoreRule { - private final static Logger LOG = LoggerFactory + private static final Logger LOG = LoggerFactory .getLogger(FastIgnoreRule.class); /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java index 864f8bfc02..d47ffc21a6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java @@ -59,7 +59,7 @@ import java.util.List; */ public class IgnoreNode { /** Result from {@link IgnoreNode#isIgnored(String, boolean)}. */ - public static enum MatchResult { + public enum MatchResult { /** The file is not ignored, due to a rule saying its not ignored. */ NOT_IGNORED, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java index 3c0f17ab3d..b7d6acce14 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java @@ -291,26 +291,25 @@ public class PathMatcher extends AbstractMatcher { // We had a prefix match here. if (!pathMatch) { return true; + } + if (right == endExcl - 1) { + // Extra slash at the end: actually a full match. + // Must meet directory expectations + return !dirOnly || assumeDirectory; + } + // Prefix matches only if pattern ended with /** + if (wasWild) { + return true; + } + if (lastWildmatch >= 0) { + // Consider pattern **/x and input x/x. + // We've matched the prefix x/ so far: we + // must try to extend the **! + matcher = lastWildmatch + 1; + right = wildmatchBacktrackPos; + wildmatchBacktrackPos = -1; } else { - if (right == endExcl - 1) { - // Extra slash at the end: actually a full match. - // Must meet directory expectations - return !dirOnly || assumeDirectory; - } - // Prefix matches only if pattern ended with /** - if (wasWild) { - return true; - } - if (lastWildmatch >= 0) { - // Consider pattern **/x and input x/x. - // We've matched the prefix x/ so far: we - // must try to extend the **! - matcher = lastWildmatch + 1; - right = wildmatchBacktrackPos; - wildmatchBacktrackPos = -1; - } else { - return false; - } + return false; } } } else if (lastWildmatch != -1) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java index 41923eed18..132109b6cc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java @@ -192,22 +192,20 @@ public class Strings { } if (pattern.indexOf('?') != -1) { return true; - } else { - // check if the backslash escapes one of the glob special characters - // if not, backslash is not part of a regex and treated literally - int backSlash = pattern.indexOf('\\'); - if (backSlash >= 0) { - int nextIdx = backSlash + 1; - if (pattern.length() == nextIdx) { - return false; - } - char nextChar = pattern.charAt(nextIdx); - if (escapedByBackslash(nextChar)) { - return true; - } else { - return false; - } + } + // check if the backslash escapes one of the glob special characters + // if not, backslash is not part of a regex and treated literally + int backSlash = pattern.indexOf('\\'); + if (backSlash >= 0) { + int nextIdx = backSlash + 1; + if (pattern.length() == nextIdx) { + return false; + } + char nextChar = pattern.charAt(nextIdx); + if (escapedByBackslash(nextChar)) { + return true; } + return false; } return false; } @@ -231,11 +229,11 @@ public class Strings { return PatternState.COMPLEX; } - static enum PatternState { + enum PatternState { LEADING_ASTERISK_ONLY, TRAILING_ASTERISK_ONLY, COMPLEX, NONE } - final static List<String> POSIX_CHAR_CLASSES = Arrays.asList( + static final List<String> POSIX_CHAR_CLASSES = Arrays.asList( "alnum", "alpha", "blank", "cntrl", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ // [:alnum:] [:alpha:] [:blank:] [:cntrl:] "digit", "graph", "lower", "print", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ @@ -250,7 +248,7 @@ public class Strings { private static final String DL = "\\p{javaDigit}\\p{javaLetter}"; //$NON-NLS-1$ - final static List<String> JAVA_CHAR_CLASSES = Arrays + static final List<String> JAVA_CHAR_CLASSES = Arrays .asList("\\p{Alnum}", "\\p{javaLetter}", "\\p{Blank}", "\\p{Cntrl}", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ // [:alnum:] [:alpha:] [:blank:] [:cntrl:] "\\p{javaDigit}", "[\\p{Graph}" + DL + "]", "\\p{Ll}", "[\\p{Print}" + DL + "]", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ @@ -263,7 +261,7 @@ public class Strings { // Collating symbols [[.a.]] or equivalence class expressions [[=a=]] are // not supported by CLI git (at least not by 1.9.1) - final static Pattern UNSUPPORTED = Pattern + static final Pattern UNSUPPORTED = Pattern .compile("\\[\\[[.=]\\w+[.=]\\]\\]"); //$NON-NLS-1$ /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index cd7d1e5a4d..37a7c7d93c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -1,45 +1,12 @@ /* * Copyright (C) 2010, 2013 Sasa Zivkov <sasa.zivkov@sap.com> - * Copyright (C) 2012, Research In Motion Limited - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2012, 2021 Research In Motion Limited and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal; @@ -345,10 +312,12 @@ public class JGitText extends TranslationBundle { /***/ public String failedAtomicFileCreation; /***/ public String failedCreateLockFile; /***/ public String failedToDetermineFilterDefinition; + /***/ public String failedToConvert; /***/ public String failedUpdatingRefs; /***/ public String failureDueToOneOfTheFollowing; /***/ public String failureUpdatingFETCH_HEAD; /***/ public String failureUpdatingTrackingRef; + /***/ public String fileAlreadyExists; /***/ public String fileCannotBeDeleted; /***/ public String fileIsTooLarge; /***/ public String fileModeNotSetForPath; @@ -387,6 +356,7 @@ public class JGitText extends TranslationBundle { /***/ public String incorrectOBJECT_ID_LENGTH; /***/ public String indexFileCorruptedNegativeBucketCount; /***/ public String indexFileIsTooLargeForJgit; + /***/ public String indexNumbersNotIncreasing; /***/ public String indexWriteException; /***/ public String initFailedBareRepoDifferentDirs; /***/ public String initFailedDirIsNoDirectory; @@ -412,6 +382,7 @@ public class JGitText extends TranslationBundle { /***/ public String invalidGitdirRef; /***/ public String invalidGitModules; /***/ public String invalidGitType; + /***/ public String invalidHooksPath; /***/ public String invalidId; /***/ public String invalidId0; /***/ public String invalidIdLength; @@ -466,10 +437,13 @@ public class JGitText extends TranslationBundle { /***/ public String localRefIsMissingObjects; /***/ public String localRepository; /***/ public String lockCountMustBeGreaterOrEqual1; + /***/ public String lockAlreadyHeld; /***/ public String lockError; /***/ public String lockFailedRetry; /***/ public String lockOnNotClosed; /***/ public String lockOnNotHeld; + /***/ public String lockStreamClosed; + /***/ public String lockStreamMultiple; /***/ public String maxCountMustBeNonNegative; /***/ public String mergeConflictOnNonNoteEntries; /***/ public String mergeConflictOnNotes; @@ -512,8 +486,10 @@ public class JGitText extends TranslationBundle { /***/ public String noMergeBase; /***/ public String noMergeHeadSpecified; /***/ public String nonBareLinkFilesNotSupported; + /***/ public String nonCommitToHeads; /***/ public String noPathAttributesFound; /***/ public String noSuchRef; + /***/ public String noSuchRefKnown; /***/ public String noSuchSubmodule; /***/ public String notABoolean; /***/ public String notABundle; @@ -612,7 +588,8 @@ public class JGitText extends TranslationBundle { /***/ public String refAlreadyExists1; /***/ public String reflogEntryNotFound; /***/ public String refNotResolved; - /***/ public String refTableRecordsMustIncrease; + /***/ public String reftableDirExists; + /***/ public String reftableRecordsMustIncrease; /***/ public String refUpdateReturnCodeWas; /***/ public String remoteConfigHasNoURIAssociated; /***/ public String remoteDoesNotHaveSpec; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java index c0364acdd1..67cb2f6942 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java @@ -126,7 +126,7 @@ public abstract class KetchLeader { private static final Logger log = LoggerFactory.getLogger(KetchLeader.class); /** Current state of the leader instance. */ - public static enum State { + public enum State { /** Newly created instance trying to elect itself leader. */ CANDIDATE, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java index 0e8377dd02..52c8f29ddc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java @@ -77,6 +77,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.SystemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -532,9 +533,8 @@ public abstract class KetchReplica { queued.add(0, new ReplicaPushRequest(this, cmds)); if (!waitingForRetry()) { - long delay = KetchSystem.delay( - lastRetryMillis, - minRetryMillis, maxRetryMillis); + long delay = FileUtils + .delay(lastRetryMillis, minRetryMillis, maxRetryMillis); if (log.isDebugEnabled()) { log.debug("Retrying {} after {} ms", //$NON-NLS-1$ describeForLog(), Long.valueOf(delay)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java index d1d4f67d86..fd334f149a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java @@ -350,25 +350,4 @@ public class KetchSystem { } } - /** - * Compute a delay in a {@code min..max} interval with random jitter. - * - * @param last - * amount of delay waited before the last attempt. This is used - * to seed the next delay interval. Should be 0 if there was no - * prior delay. - * @param min - * shortest amount of allowable delay between attempts. - * @param max - * longest amount of allowable delay between attempts. - * @return new amount of delay to wait before the next attempt. - */ - static long delay(long last, long min, long max) { - long r = Math.max(0, last * 3 - min); - if (r > 0) { - int c = (int) Math.min(r + 1, Integer.MAX_VALUE); - r = RNG.nextInt(c); - } - return Math.max(Math.min(min + r, max), min); - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java index 53fd198006..a27a9bc446 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java @@ -132,9 +132,8 @@ class LagCheck implements AutoCloseable { // TODO(sop) Check term to see if my leader was deposed. if (rw.isMergedInto(head, remote)) { return AHEAD; - } else { - return DIVERGENT; } + return DIVERGENT; } catch (IOException err) { KetchReplica.log.error(String.format( "Cannot compare %s", //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java index c6e2fae42f..16e7a0d537 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java @@ -412,8 +412,7 @@ public final class DfsBlockCache { getStat(statMiss, key).incrementAndGet(); boolean credit = true; try { - v = file.readOneBlock(requestedPosition, ctx, - fileChannel.get()); + v = file.readOneBlock(position, ctx, fileChannel.get()); credit = false; } finally { if (credit) { @@ -450,7 +449,7 @@ public final class DfsBlockCache { } @SuppressWarnings("unchecked") - private void reserveSpace(int reserve, DfsStreamKey key) { + private void reserveSpace(long reserve, DfsStreamKey key) { clockLock.lock(); try { long live = LongStream.of(getCurrentSize()).sum() + reserve; @@ -487,7 +486,7 @@ public final class DfsBlockCache { } } - private void creditSpace(int credit, DfsStreamKey key) { + private void creditSpace(long credit, DfsStreamKey key) { clockLock.lock(); try { getStat(liveBytes, key).addAndGet(-credit); @@ -497,7 +496,7 @@ public final class DfsBlockCache { } @SuppressWarnings("unchecked") - private void addToClock(Ref ref, int credit) { + private void addToClock(Ref ref, long credit) { clockLock.lock(); try { if (credit != 0) { @@ -521,17 +520,20 @@ public final class DfsBlockCache { * * @param key * the stream key of the pack. + * @param position + * the position in the key. The default should be 0. * @param loader * the function to load the reference. * @return the object reference. * @throws IOException * the reference was not in the cache and could not be loaded. */ - <T> Ref<T> getOrLoadRef(DfsStreamKey key, RefLoader<T> loader) + <T> Ref<T> getOrLoadRef( + DfsStreamKey key, long position, RefLoader<T> loader) throws IOException { - int slot = slot(key, 0); + int slot = slot(key, position); HashEntry e1 = table.get(slot); - Ref<T> ref = scanRef(e1, key, 0); + Ref<T> ref = scanRef(e1, key, position); if (ref != null) { getStat(statHit, key).incrementAndGet(); return ref; @@ -543,7 +545,7 @@ public final class DfsBlockCache { try { HashEntry e2 = table.get(slot); if (e2 != e1) { - ref = scanRef(e2, key, 0); + ref = scanRef(e2, key, position); if (ref != null) { getStat(statHit, key).incrementAndGet(); return ref; @@ -574,10 +576,10 @@ public final class DfsBlockCache { } <T> Ref<T> putRef(DfsStreamKey key, long size, T v) { - return put(key, 0, (int) Math.min(size, Integer.MAX_VALUE), v); + return put(key, 0, size, v); } - <T> Ref<T> put(DfsStreamKey key, long pos, int size, T v) { + <T> Ref<T> put(DfsStreamKey key, long pos, long size, T v) { int slot = slot(key, pos); HashEntry e1 = table.get(slot); Ref<T> ref = scanRef(e1, key, pos); @@ -720,12 +722,12 @@ public final class DfsBlockCache { static final class Ref<T> { final DfsStreamKey key; final long position; - final int size; + final long size; volatile T value; Ref next; volatile boolean hot; - Ref(DfsStreamKey key, long position, int size, T v) { + Ref(DfsStreamKey key, long position, long size, T v) { this.key = key; this.position = position; this.size = size; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java index 3605236e56..9b280747df 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java @@ -61,6 +61,13 @@ public class DfsCachedPack extends CachedPack { } /** + * @return the pack passed to the constructor + */ + public DfsPackFile getPackFile() { + return pack; + } + + /** * Get the description of the pack. * * @return the description of the pack. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java index f10a1d8127..3e71d079b5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java @@ -744,11 +744,15 @@ public class DfsGarbageCollector { return; } - try (ReftableStack stack = ReftableStack.open(ctx, reftablesBefore)) { - ReftableCompactor compact = new ReftableCompactor(); + try (DfsReftableStack stack = DfsReftableStack.open(ctx, reftablesBefore); + DfsOutputStream out = objdb.writeFile(pack, REFTABLE)) { + ReftableCompactor compact = new ReftableCompactor(out); compact.addAll(stack.readers()); compact.setIncludeDeletes(includeDeletes); - compactReftable(pack, compact); + compact.setConfig(configureReftable(reftableConfig, out)); + compact.compact(); + pack.addFileExt(REFTABLE); + pack.setReftableStats(compact.getStats()); } } @@ -765,24 +769,12 @@ public class DfsGarbageCollector { throws IOException { try (DfsOutputStream out = objdb.writeFile(pack, REFTABLE)) { ReftableConfig cfg = configureReftable(reftableConfig, out); - ReftableWriter writer = new ReftableWriter(cfg) + ReftableWriter writer = new ReftableWriter(cfg, out) .setMinUpdateIndex(reftableInitialMinUpdateIndex) - .setMaxUpdateIndex(reftableInitialMaxUpdateIndex) - .begin(out) - .sortAndWriteRefs(refs) - .finish(); + .setMaxUpdateIndex(reftableInitialMaxUpdateIndex).begin() + .sortAndWriteRefs(refs).finish(); pack.addFileExt(REFTABLE); pack.setReftableStats(writer.getStats()); } } - - private void compactReftable(DfsPackDescription pack, - ReftableCompactor compact) throws IOException { - try (DfsOutputStream out = objdb.writeFile(pack, REFTABLE)) { - compact.setConfig(configureReftable(reftableConfig, out)); - compact.compact(out); - pack.addFileExt(REFTABLE); - pack.setReftableStats(compact.getStats()); - } - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java index 09d59376a0..0ee8135faf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java @@ -97,7 +97,7 @@ public abstract class DfsObjDatabase extends ObjectDatabase { * comparator based on {@link Enum#compareTo}. Prefer {@link * #DEFAULT_COMPARATOR} or your own {@link ComparatorBuilder}. */ - public static enum PackSource { + public enum PackSource { /** The pack is created by ObjectInserter due to local activity. */ INSERT, @@ -705,7 +705,7 @@ public abstract class DfsObjDatabase extends ObjectDatabase { } /** Snapshot of packs scanned in a single pass. */ - public static abstract class PackList { + public abstract static class PackList { /** All known packs, sorted. */ public final DfsPackFile[] packs; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java index 6f3f2bd8e7..083124e5ed 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java @@ -311,12 +311,16 @@ public class DfsPackCompactor { DfsObjDatabase objdb = repo.getObjectDatabase(); Collections.sort(srcReftables, objdb.reftableComparator()); - try (ReftableStack stack = ReftableStack.open(ctx, srcReftables)) { - initOutDesc(objdb); - ReftableCompactor compact = new ReftableCompactor(); + initOutDesc(objdb); + try (DfsReftableStack stack = DfsReftableStack.open(ctx, srcReftables); + DfsOutputStream out = objdb.writeFile(outDesc, REFTABLE)) { + ReftableCompactor compact = new ReftableCompactor(out); compact.addAll(stack.readers()); compact.setIncludeDeletes(true); - writeReftable(objdb, outDesc, compact); + compact.setConfig(configureReftable(reftableConfig, out)); + compact.compact(); + outDesc.addFileExt(REFTABLE); + outDesc.setReftableStats(compact.getStats()); } } @@ -497,16 +501,6 @@ public class DfsPackCompactor { } } - private void writeReftable(DfsObjDatabase objdb, DfsPackDescription pack, - ReftableCompactor compact) throws IOException { - try (DfsOutputStream out = objdb.writeFile(pack, REFTABLE)) { - compact.setConfig(configureReftable(reftableConfig, out)); - compact.compact(out); - pack.addFileExt(REFTABLE); - pack.setReftableStats(compact.getStats()); - } - } - static ReftableConfig configureReftable(ReftableConfig cfg, DfsOutputStream out) { int bs = out.blockSize(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java index be1387ed0c..d0f9b1c1f4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java @@ -89,6 +89,7 @@ import org.eclipse.jgit.util.LongList; */ public final class DfsPackFile extends BlockBasedFile { private static final int REC_SIZE = Constants.OBJECT_ID_LENGTH + 8; + private static final long REF_POSITION = 0; /** * Lock for initialization of {@link #index} and {@link #corruptObjects}. @@ -194,45 +195,10 @@ public final class DfsPackFile extends BlockBasedFile { try { DfsStreamKey idxKey = desc.getStreamKey(INDEX); - DfsBlockCache.Ref<PackIndex> idxref = cache.getOrLoadRef(idxKey, - () -> { - try { - ctx.stats.readIdx++; - long start = System.nanoTime(); - try (ReadableChannel rc = ctx.db.openFile(desc, - INDEX)) { - InputStream in = Channels - .newInputStream(rc); - int wantSize = 8192; - int bs = rc.blockSize(); - if (0 < bs && bs < wantSize) { - bs = (wantSize / bs) * bs; - } else if (bs <= 0) { - bs = wantSize; - } - PackIndex idx = PackIndex.read( - new BufferedInputStream(in, bs)); - int sz = (int) Math.min( - idx.getObjectCount() * REC_SIZE, - Integer.MAX_VALUE); - ctx.stats.readIdxBytes += rc.position(); - index = idx; - return new DfsBlockCache.Ref<>(idxKey, 0, - sz, idx); - } finally { - ctx.stats.readIdxMicros += elapsedMicros( - start); - } - } catch (EOFException e) { - throw new IOException(MessageFormat.format( - DfsText.get().shortReadOfIndex, - desc.getFileName(INDEX)), e); - } catch (IOException e) { - throw new IOException(MessageFormat.format( - DfsText.get().cannotReadIndex, - desc.getFileName(INDEX)), e); - } - }); + DfsBlockCache.Ref<PackIndex> idxref = cache.getOrLoadRef( + idxKey, + REF_POSITION, + () -> loadPackIndex(ctx, idxKey)); PackIndex idx = idxref.get(); if (index == null && idx != null) { index = idx; @@ -267,44 +233,10 @@ public final class DfsPackFile extends BlockBasedFile { PackIndex idx = idx(ctx); PackReverseIndex revidx = getReverseIdx(ctx); DfsStreamKey bitmapKey = desc.getStreamKey(BITMAP_INDEX); - DfsBlockCache.Ref<PackBitmapIndex> idxref = cache - .getOrLoadRef(bitmapKey, () -> { - ctx.stats.readBitmap++; - long start = System.nanoTime(); - try (ReadableChannel rc = ctx.db.openFile(desc, - BITMAP_INDEX)) { - long size; - PackBitmapIndex bmidx; - try { - InputStream in = Channels.newInputStream(rc); - int wantSize = 8192; - int bs = rc.blockSize(); - if (0 < bs && bs < wantSize) { - bs = (wantSize / bs) * bs; - } else if (bs <= 0) { - bs = wantSize; - } - in = new BufferedInputStream(in, bs); - bmidx = PackBitmapIndex.read(in, idx, revidx); - } finally { - size = rc.position(); - ctx.stats.readIdxBytes += size; - ctx.stats.readIdxMicros += elapsedMicros(start); - } - int sz = (int) Math.min(size, Integer.MAX_VALUE); - bitmapIndex = bmidx; - return new DfsBlockCache.Ref<>(bitmapKey, 0, sz, - bmidx); - } catch (EOFException e) { - throw new IOException(MessageFormat.format( - DfsText.get().shortReadOfIndex, - desc.getFileName(BITMAP_INDEX)), e); - } catch (IOException e) { - throw new IOException(MessageFormat.format( - DfsText.get().cannotReadIndex, - desc.getFileName(BITMAP_INDEX)), e); - } - }); + DfsBlockCache.Ref<PackBitmapIndex> idxref = cache.getOrLoadRef( + bitmapKey, + REF_POSITION, + () -> loadBitmapIndex(ctx, bitmapKey, idx, revidx)); PackBitmapIndex bmidx = idxref.get(); if (bitmapIndex == null && bmidx != null) { bitmapIndex = bmidx; @@ -326,14 +258,10 @@ public final class DfsPackFile extends BlockBasedFile { PackIndex idx = idx(ctx); DfsStreamKey revKey = new DfsStreamKey.ForReverseIndex( desc.getStreamKey(INDEX)); - DfsBlockCache.Ref<PackReverseIndex> revref = cache - .getOrLoadRef(revKey, () -> { - PackReverseIndex revidx = new PackReverseIndex(idx); - int sz = (int) Math.min(idx.getObjectCount() * 8, - Integer.MAX_VALUE); - reverseIndex = revidx; - return new DfsBlockCache.Ref<>(revKey, 0, sz, revidx); - }); + DfsBlockCache.Ref<PackReverseIndex> revref = cache.getOrLoadRef( + revKey, + REF_POSITION, + () -> loadReverseIdx(revKey, idx)); PackReverseIndex revidx = revref.get(); if (reverseIndex == null && revidx != null) { reverseIndex = revidx; @@ -1091,4 +1019,91 @@ public final class DfsPackFile extends BlockBasedFile { list.add(offset); } } + + private DfsBlockCache.Ref<PackIndex> loadPackIndex( + DfsReader ctx, DfsStreamKey idxKey) throws IOException { + try { + ctx.stats.readIdx++; + long start = System.nanoTime(); + try (ReadableChannel rc = ctx.db.openFile(desc, INDEX)) { + InputStream in = Channels.newInputStream(rc); + int wantSize = 8192; + int bs = rc.blockSize(); + if (0 < bs && bs < wantSize) { + bs = (wantSize / bs) * bs; + } else if (bs <= 0) { + bs = wantSize; + } + PackIndex idx = PackIndex.read(new BufferedInputStream(in, bs)); + ctx.stats.readIdxBytes += rc.position(); + index = idx; + return new DfsBlockCache.Ref<>( + idxKey, + REF_POSITION, + idx.getObjectCount() * REC_SIZE, + idx); + } finally { + ctx.stats.readIdxMicros += elapsedMicros(start); + } + } catch (EOFException e) { + throw new IOException(MessageFormat.format( + DfsText.get().shortReadOfIndex, + desc.getFileName(INDEX)), e); + } catch (IOException e) { + throw new IOException(MessageFormat.format( + DfsText.get().cannotReadIndex, + desc.getFileName(INDEX)), e); + } + } + + private DfsBlockCache.Ref<PackReverseIndex> loadReverseIdx( + DfsStreamKey revKey, PackIndex idx) { + PackReverseIndex revidx = new PackReverseIndex(idx); + reverseIndex = revidx; + return new DfsBlockCache.Ref<>( + revKey, + REF_POSITION, + idx.getObjectCount() * 8, + revidx); + } + + private DfsBlockCache.Ref<PackBitmapIndex> loadBitmapIndex( + DfsReader ctx, + DfsStreamKey bitmapKey, + PackIndex idx, + PackReverseIndex revidx) throws IOException { + ctx.stats.readBitmap++; + long start = System.nanoTime(); + try (ReadableChannel rc = ctx.db.openFile(desc, BITMAP_INDEX)) { + long size; + PackBitmapIndex bmidx; + try { + InputStream in = Channels.newInputStream(rc); + int wantSize = 8192; + int bs = rc.blockSize(); + if (0 < bs && bs < wantSize) { + bs = (wantSize / bs) * bs; + } else if (bs <= 0) { + bs = wantSize; + } + in = new BufferedInputStream(in, bs); + bmidx = PackBitmapIndex.read(in, idx, revidx); + } finally { + size = rc.position(); + ctx.stats.readIdxBytes += size; + ctx.stats.readIdxMicros += elapsedMicros(start); + } + bitmapIndex = bmidx; + return new DfsBlockCache.Ref<>( + bitmapKey, REF_POSITION, size, bmidx); + } catch (EOFException e) { + throw new IOException(MessageFormat.format( + DfsText.get().shortReadOfIndex, + desc.getFileName(BITMAP_INDEX)), e); + } catch (IOException e) { + throw new IOException(MessageFormat.format( + DfsText.get().cannotReadIndex, + desc.getFileName(BITMAP_INDEX)), e); + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java index 732cd4d1c6..b3b9e39375 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java @@ -191,14 +191,11 @@ public abstract class DfsRefDatabase extends RefDatabase { rw.peel(obj).copy(), hasVersioning() ? leaf.getUpdateIndex() : UNDEFINED_UPDATE_INDEX); - } else { - return new ObjectIdRef.PeeledNonTag( - leaf.getStorage(), - leaf.getName(), - leaf.getObjectId(), - hasVersioning() ? leaf.getUpdateIndex() - : UNDEFINED_UPDATE_INDEX); } + return new ObjectIdRef.PeeledNonTag(leaf.getStorage(), + leaf.getName(), leaf.getObjectId(), + hasVersioning() ? leaf.getUpdateIndex() + : UNDEFINED_UPDATE_INDEX); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableBatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableBatchRefUpdate.java new file mode 100644 index 0000000000..124630edb7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableBatchRefUpdate.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2019, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.dfs; + +import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; +import org.eclipse.jgit.internal.storage.io.BlockSource; +import org.eclipse.jgit.internal.storage.pack.PackExt; +import org.eclipse.jgit.internal.storage.reftable.ReftableBatchRefUpdate; +import org.eclipse.jgit.internal.storage.reftable.ReftableCompactor; +import org.eclipse.jgit.internal.storage.reftable.ReftableConfig; +import org.eclipse.jgit.internal.storage.reftable.ReftableReader; +import org.eclipse.jgit.internal.storage.reftable.ReftableWriter; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.transport.ReceiveCommand; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE; + +/** + * {@link org.eclipse.jgit.lib.BatchRefUpdate} for + * {@link org.eclipse.jgit.internal.storage.dfs.DfsReftableDatabase}. + */ +public class DfsReftableBatchRefUpdate extends ReftableBatchRefUpdate { + private static final int AVG_BYTES = 36; + + private final DfsReftableDatabase refdb; + + private final DfsObjDatabase odb; + + /** + * Initialize batch update. + * + * @param refdb + * database the update will modify. + * @param odb + * object database to store the reftable. + */ + protected DfsReftableBatchRefUpdate(DfsReftableDatabase refdb, + DfsObjDatabase odb) { + super(refdb, refdb.reftableDatabase, refdb.getLock(), refdb.getRepository()); + this.refdb = refdb; + this.odb = odb; + } + + @Override + protected void applyUpdates(List<Ref> newRefs, List<ReceiveCommand> pending) + throws IOException { + Set<DfsPackDescription> prune = Collections.emptySet(); + DfsPackDescription pack = odb.newPack(PackSource.INSERT); + try (DfsOutputStream out = odb.writeFile(pack, REFTABLE)) { + ReftableConfig cfg = DfsPackCompactor + .configureReftable(refdb.getReftableConfig(), out); + + ReftableWriter.Stats stats; + if (refdb.compactDuringCommit() + && newRefs.size() * AVG_BYTES <= cfg.getRefBlockSize() + && canCompactTopOfStack(cfg)) { + ByteArrayOutputStream tmp = new ByteArrayOutputStream(); + ReftableWriter rw = new ReftableWriter(cfg, tmp); + write(rw, newRefs, pending); + rw.finish(); + stats = compactTopOfStack(out, cfg, tmp.toByteArray()); + prune = toPruneTopOfStack(); + } else { + ReftableWriter rw = new ReftableWriter(cfg, out); + write(rw, newRefs, pending); + rw.finish(); + stats = rw.getStats(); + } + pack.addFileExt(REFTABLE); + pack.setReftableStats(stats); + } + + odb.commitPack(Collections.singleton(pack), prune); + odb.addReftable(pack, prune); + refdb.clearCache(); + } + + private boolean canCompactTopOfStack(ReftableConfig cfg) + throws IOException { + refdb.getLock().lock(); + try { + DfsReftableStack stack = refdb.stack(); + List<ReftableReader> readers = stack.readers(); + if (readers.isEmpty()) { + return false; + } + + int lastIdx = readers.size() - 1; + DfsReftable last = stack.files().get(lastIdx); + DfsPackDescription desc = last.getPackDescription(); + if (desc.getPackSource() != PackSource.INSERT + || !packOnlyContainsReftable(desc)) { + return false; + } + + ReftableReader table = readers.get(lastIdx); + int bs = cfg.getRefBlockSize(); + return table.size() <= 3 * bs; + } finally { + refdb.getLock().unlock(); + } + } + + private ReftableWriter.Stats compactTopOfStack(OutputStream out, + ReftableConfig cfg, byte[] newTable) throws IOException { + refdb.getLock().lock(); + try { + List<ReftableReader> stack = refdb.stack().readers(); + + ReftableReader last = stack.get(stack.size() - 1); + + List<ReftableReader> tables = new ArrayList<>(2); + tables.add(last); + tables.add(new ReftableReader(BlockSource.from(newTable))); + + ReftableCompactor compactor = new ReftableCompactor(out); + compactor.setConfig(cfg); + compactor.setIncludeDeletes(true); + compactor.addAll(tables); + compactor.compact(); + return compactor.getStats(); + } finally { + refdb.getLock().unlock(); + } + } + + private Set<DfsPackDescription> toPruneTopOfStack() throws IOException { + refdb.getLock().lock(); + try { + List<DfsReftable> stack = refdb.stack().files(); + + DfsReftable last = stack.get(stack.size() - 1); + return Collections.singleton(last.getPackDescription()); + } finally { + refdb.getLock().unlock(); + } + } + + private boolean packOnlyContainsReftable(DfsPackDescription desc) { + for (PackExt ext : PackExt.values()) { + if (ext != REFTABLE && desc.hasFileExt(ext)) { + return false; + } + } + return true; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java index 6050c15992..124131d1d3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableDatabase.java @@ -45,19 +45,17 @@ package org.eclipse.jgit.internal.storage.dfs; import java.io.IOException; import java.util.Arrays; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.internal.storage.reftable.MergedReftable; -import org.eclipse.jgit.internal.storage.reftable.RefCursor; -import org.eclipse.jgit.internal.storage.reftable.Reftable; import org.eclipse.jgit.internal.storage.reftable.ReftableConfig; +import org.eclipse.jgit.internal.storage.reftable.ReftableDatabase; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; @@ -81,13 +79,10 @@ import org.eclipse.jgit.util.RefMap; * and one will fail. */ public class DfsReftableDatabase extends DfsRefDatabase { - private final ReentrantLock lock = new ReentrantLock(true); + final ReftableDatabase reftableDatabase; private DfsReader ctx; - - private ReftableStack tableStack; - - private MergedReftable mergedTables; + private DfsReftableStack stack; /** * Initialize the reference database for a repository. @@ -97,6 +92,18 @@ public class DfsReftableDatabase extends DfsRefDatabase { */ protected DfsReftableDatabase(DfsRepository repo) { super(repo); + reftableDatabase = new ReftableDatabase() { + @Override + public MergedReftable openMergedReftable() throws IOException { + DfsReftableDatabase.this.getLock().lock(); + try { + return new MergedReftable(stack().readers()); + } finally { + DfsReftableDatabase.this.getLock().unlock(); + } + } + }; + stack = null; } /** {@inheritDoc} */ @@ -115,7 +122,7 @@ public class DfsReftableDatabase extends DfsRefDatabase { @Override public BatchRefUpdate newBatchUpdate() { DfsObjDatabase odb = getRepository().getObjectDatabase(); - return new ReftableBatchRefUpdate(this, odb); + return new DfsReftableBatchRefUpdate(this, odb); } /** @@ -124,7 +131,7 @@ public class DfsReftableDatabase extends DfsRefDatabase { * @return configuration to write new reftables with. */ public ReftableConfig getReftableConfig() { - return new ReftableConfig(getRepository().getConfig()); + return new ReftableConfig(getRepository()); } /** @@ -133,7 +140,7 @@ public class DfsReftableDatabase extends DfsRefDatabase { * @return the lock protecting this instance's state. */ protected ReentrantLock getLock() { - return lock; + return reftableDatabase.getLock(); } /** @@ -147,134 +154,57 @@ public class DfsReftableDatabase extends DfsRefDatabase { return true; } - /** - * Obtain a handle to the merged reader. - * - * @return (possibly cached) handle to the merged reader. - * @throws java.io.IOException - * if tables cannot be opened. - */ - protected Reftable reader() throws IOException { - lock.lock(); - try { - if (mergedTables == null) { - mergedTables = new MergedReftable(stack().readers()); - } - return mergedTables; - } finally { - lock.unlock(); - } - } /** - * Obtain a handle to the stack of reftables. + * Obtain a handle to the stack of reftables. Must hold lock. * * @return (possibly cached) handle to the stack. * @throws java.io.IOException * if tables cannot be opened. */ - protected ReftableStack stack() throws IOException { - lock.lock(); - try { - if (tableStack == null) { - DfsObjDatabase odb = getRepository().getObjectDatabase(); - if (ctx == null) { - ctx = odb.newReader(); - } - tableStack = ReftableStack.open(ctx, - Arrays.asList(odb.getReftables())); - } - return tableStack; - } finally { - lock.unlock(); + protected DfsReftableStack stack() throws IOException { + if (!getLock().isLocked()) { + throw new IllegalStateException("most hold lock to access stack"); //$NON-NLS-1$ } + DfsObjDatabase odb = getRepository().getObjectDatabase(); + + if (ctx == null) { + ctx = odb.newReader(); + } + if (stack == null) { + stack = DfsReftableStack.open(ctx, Arrays.asList(odb.getReftables())); + } + return stack; } - /** {@inheritDoc} */ @Override public boolean isNameConflicting(String refName) throws IOException { - lock.lock(); - try { - Reftable table = reader(); - - // Cannot be nested within an existing reference. - int lastSlash = refName.lastIndexOf('/'); - while (0 < lastSlash) { - if (table.hasRef(refName.substring(0, lastSlash))) { - return true; - } - lastSlash = refName.lastIndexOf('/', lastSlash - 1); - } - - // Cannot be the container of an existing reference. - return table.hasRefsWithPrefix(refName + '/'); - } finally { - lock.unlock(); - } + return reftableDatabase.isNameConflicting(refName, new TreeSet<>(), new HashSet<>()); } /** {@inheritDoc} */ @Override public Ref exactRef(String name) throws IOException { - lock.lock(); - try { - Reftable table = reader(); - Ref ref = table.exactRef(name); - if (ref != null && ref.isSymbolic()) { - return table.resolve(ref); - } - return ref; - } finally { - lock.unlock(); - } + return reftableDatabase.exactRef(name); } /** {@inheritDoc} */ @Override public Map<String, Ref> getRefs(String prefix) throws IOException { - RefList.Builder<Ref> all = new RefList.Builder<>(); - lock.lock(); - try { - Reftable table = reader(); - try (RefCursor rc = ALL.equals(prefix) ? table.allRefs() - : (prefix.endsWith("/") ? table.seekRefsWithPrefix(prefix) //$NON-NLS-1$ - : table.seekRef(prefix))) { - while (rc.next()) { - Ref ref = table.resolve(rc.getRef()); - if (ref != null && ref.getObjectId() != null) { - all.add(ref); - } - } - } - } finally { - lock.unlock(); + List<Ref> refs = reftableDatabase.getRefsByPrefix(prefix); + RefList.Builder<Ref> builder = new RefList.Builder<>(refs.size()); + for (Ref r : refs) { + builder.add(r); } - - RefList<Ref> none = RefList.emptyList(); - return new RefMap(prefix, all.toRefList(), none, none); + return new RefMap(prefix, builder.toRefList(), RefList.emptyList(), + RefList.emptyList()); } /** {@inheritDoc} */ @Override public List<Ref> getRefsByPrefix(String prefix) throws IOException { - List<Ref> all = new ArrayList<>(); - lock.lock(); - try { - Reftable table = reader(); - try (RefCursor rc = ALL.equals(prefix) ? table.allRefs() - : table.seekRefsWithPrefix(prefix)) { - while (rc.next()) { - Ref ref = table.resolve(rc.getRef()); - if (ref != null && ref.getObjectId() != null) { - all.add(ref); - } - } - } - } finally { - lock.unlock(); - } - return Collections.unmodifiableList(all); + return reftableDatabase.getRefsByPrefix(prefix); } /** {@inheritDoc} */ @@ -283,17 +213,13 @@ public class DfsReftableDatabase extends DfsRefDatabase { if (!getReftableConfig().isIndexObjects()) { return super.getTipsWithSha1(id); } - lock.lock(); - try { - RefCursor cursor = reader().byObjectId(id); - Set<Ref> refs = new HashSet<>(); - while (cursor.next()) { - refs.add(cursor.getRef()); - } - return refs; - } finally { - lock.unlock(); - } + return reftableDatabase.getTipsWithSha1(id); + } + + /** {@inheritDoc} */ + @Override + public boolean hasFastTipsWithSha1() throws IOException { + return reftableDatabase.hasFastTipsWithSha1(); } /** {@inheritDoc} */ @@ -314,19 +240,19 @@ public class DfsReftableDatabase extends DfsRefDatabase { @Override void clearCache() { - lock.lock(); + getLock().lock(); try { - if (tableStack != null) { - tableStack.close(); - tableStack = null; - } if (ctx != null) { ctx.close(); ctx = null; } - mergedTables = null; + reftableDatabase.clearCache(); + if (stack != null) { + stack.close(); + stack = null; + } } finally { - lock.unlock(); + getLock().unlock(); } } @@ -334,7 +260,7 @@ public class DfsReftableDatabase extends DfsRefDatabase { @Override protected boolean compareAndPut(Ref oldRef, @Nullable Ref newRef) throws IOException { - ReceiveCommand cmd = toCommand(oldRef, newRef); + ReceiveCommand cmd = ReftableDatabase.toCommand(oldRef, newRef); try (RevWalk rw = new RevWalk(getRepository())) { rw.setRetainBody(false); newBatchUpdate().setAllowNonFastForwards(true).addCommand(cmd) @@ -351,58 +277,6 @@ public class DfsReftableDatabase extends DfsRefDatabase { } } - private static ReceiveCommand toCommand(Ref oldRef, Ref newRef) { - ObjectId oldId = toId(oldRef); - ObjectId newId = toId(newRef); - String name = toName(oldRef, newRef); - - if (oldRef != null && oldRef.isSymbolic()) { - if (newRef != null) { - if (newRef.isSymbolic()) { - return ReceiveCommand.link(oldRef.getTarget().getName(), - newRef.getTarget().getName(), name); - } else { - return ReceiveCommand.unlink(oldRef.getTarget().getName(), - newId, name); - } - } else { - return ReceiveCommand.unlink(oldRef.getTarget().getName(), - ObjectId.zeroId(), name); - } - } - - if (newRef != null && newRef.isSymbolic()) { - if (oldRef != null) { - if (oldRef.isSymbolic()) { - return ReceiveCommand.link(oldRef.getTarget().getName(), - newRef.getTarget().getName(), name); - } else { - return ReceiveCommand.link(oldId, - newRef.getTarget().getName(), name); - } - } else { - return ReceiveCommand.link(ObjectId.zeroId(), - newRef.getTarget().getName(), name); - } - } - - return new ReceiveCommand(oldId, newId, name); - } - - private static ObjectId toId(Ref ref) { - if (ref != null) { - ObjectId id = ref.getObjectId(); - if (id != null) { - return id; - } - } - return ObjectId.zeroId(); - } - - private static String toName(Ref oldRef, Ref newRef) { - return oldRef != null ? oldRef.getName() : newRef.getName(); - } - /** {@inheritDoc} */ @Override protected boolean compareAndRemove(Ref oldRef) throws IOException { @@ -417,12 +291,12 @@ public class DfsReftableDatabase extends DfsRefDatabase { @Override void stored(Ref ref) { - // Unnecessary; ReftableBatchRefUpdate calls clearCache(). + // Unnecessary; DfsReftableBatchRefUpdate calls clearCache(). } @Override void removed(String refName) { - // Unnecessary; ReftableBatchRefUpdate calls clearCache(). + // Unnecessary; DfsReftableBatchRefUpdate calls clearCache(). } /** {@inheritDoc} */ @@ -430,4 +304,5 @@ public class DfsReftableDatabase extends DfsRefDatabase { protected void cachePeeledState(Ref oldLeaf, Ref newLeaf) { // Do not cache peeled state in reftable. } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReftableStack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableStack.java index 50ba0e0f38..59621a4e67 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReftableStack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReftableStack.java @@ -48,13 +48,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.eclipse.jgit.internal.storage.reftable.Reftable; +import org.eclipse.jgit.internal.storage.reftable.ReftableReader; /** * Tracks multiple open - * {@link org.eclipse.jgit.internal.storage.reftable.Reftable} instances. + * {@link org.eclipse.jgit.internal.storage.reftable.ReftableReader} instances. */ -public class ReftableStack implements AutoCloseable { +public class DfsReftableStack implements AutoCloseable { /** * Opens a stack of tables for reading. * @@ -67,9 +67,9 @@ public class ReftableStack implements AutoCloseable { * @throws java.io.IOException * a table could not be opened */ - public static ReftableStack open(DfsReader ctx, List<DfsReftable> files) + public static DfsReftableStack open(DfsReader ctx, List<DfsReftable> files) throws IOException { - ReftableStack stack = new ReftableStack(files.size()); + DfsReftableStack stack = new DfsReftableStack(files.size()); boolean close = true; try { for (DfsReftable t : files) { @@ -86,9 +86,9 @@ public class ReftableStack implements AutoCloseable { } private final List<DfsReftable> files; - private final List<Reftable> tables; + private final List<ReftableReader> tables; - private ReftableStack(int tableCnt) { + private DfsReftableStack(int tableCnt) { this.files = new ArrayList<>(tableCnt); this.tables = new ArrayList<>(tableCnt); } @@ -109,14 +109,14 @@ public class ReftableStack implements AutoCloseable { * @return unmodifiable list of tables, in the same order the files were * passed to {@link #open(DfsReader, List)}. */ - public List<Reftable> readers() { + public List<ReftableReader> readers() { return Collections.unmodifiableList(tables); } /** {@inheritDoc} */ @Override public void close() { - for (Reftable t : tables) { + for (ReftableReader t : tables) { try { t.close(); } catch (IOException e) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java index 1f6b20aacf..db5b1279be 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileObjectDatabase.java @@ -60,7 +60,7 @@ import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.util.FS; abstract class FileObjectDatabase extends ObjectDatabase { - static enum InsertLooseObjectResult { + enum InsertLooseObjectResult { INSERTED, EXISTS_PACKED, EXISTS_LOOSE, FAILURE; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java new file mode 100644 index 0000000000..e5ffd77c5d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java @@ -0,0 +1,669 @@ +/* + * Copyright (C) 2019 Google LLC + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.file; + +import static org.eclipse.jgit.lib.Ref.UNDEFINED_UPDATE_INDEX; +import static org.eclipse.jgit.lib.Ref.Storage.NEW; +import static org.eclipse.jgit.lib.Ref.Storage.PACKED; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.events.RefsChangedEvent; +import org.eclipse.jgit.internal.storage.reftable.MergedReftable; +import org.eclipse.jgit.internal.storage.reftable.ReftableBatchRefUpdate; +import org.eclipse.jgit.internal.storage.reftable.ReftableDatabase; +import org.eclipse.jgit.internal.storage.reftable.ReftableWriter; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefRename; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.ReflogEntry; +import org.eclipse.jgit.lib.ReflogReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.SymbolicRef; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.RefList; +import org.eclipse.jgit.util.RefMap; + +/** + * Implements RefDatabase using reftable for storage. + * + * This class is threadsafe. + */ +public class FileReftableDatabase extends RefDatabase { + private final ReftableDatabase reftableDatabase; + + private final FileRepository fileRepository; + + private final FileReftableStack reftableStack; + + FileReftableDatabase(FileRepository repo) throws IOException { + this(repo, new File(new File(repo.getDirectory(), Constants.REFTABLE), + Constants.TABLES_LIST)); + } + + FileReftableDatabase(FileRepository repo, File refstackName) throws IOException { + this.fileRepository = repo; + this.reftableStack = new FileReftableStack(refstackName, + new File(fileRepository.getDirectory(), Constants.REFTABLE), + () -> fileRepository.fireEvent(new RefsChangedEvent()), + () -> fileRepository.getConfig()); + this.reftableDatabase = new ReftableDatabase() { + + @Override + public MergedReftable openMergedReftable() throws IOException { + return reftableStack.getMergedReftable(); + } + }; + } + + ReflogReader getReflogReader(String refname) throws IOException { + return reftableDatabase.getReflogReader(refname); + } + + /** + * @param repoDir + * @return whether the given repo uses reftable for refdb storage. + */ + public static boolean isReftable(File repoDir) { + return new File(repoDir, Constants.REFTABLE).isDirectory(); + } + + /** {@inheritDoc} */ + @Override + public boolean hasFastTipsWithSha1() throws IOException { + return reftableDatabase.hasFastTipsWithSha1(); + } + + /** + * Runs a full compaction for GC purposes. + * @throws IOException on I/O errors + */ + public void compactFully() throws IOException { + reftableDatabase.getLock().lock(); + try { + reftableStack.compactFully(); + reftableDatabase.clearCache(); + } finally { + reftableDatabase.getLock().unlock(); + } + } + + private ReentrantLock getLock() { + return reftableDatabase.getLock(); + } + + /** {@inheritDoc} */ + @Override + public boolean performsAtomicTransactions() { + return true; + } + + /** {@inheritDoc} */ + @NonNull + @Override + public BatchRefUpdate newBatchUpdate() { + return new FileReftableBatchRefUpdate(this, fileRepository); + } + + /** {@inheritDoc} */ + @Override + public RefUpdate newUpdate(String refName, boolean detach) + throws IOException { + boolean detachingSymbolicRef = false; + Ref ref = exactRef(refName); + + if (ref == null) { + ref = new ObjectIdRef.Unpeeled(NEW, refName, null); + } else { + detachingSymbolicRef = detach && ref.isSymbolic(); + } + + RefUpdate update = new FileReftableRefUpdate(ref); + if (detachingSymbolicRef) { + update.setDetachingSymbolicRef(); + } + return update; + } + + /** {@inheritDoc} */ + @Override + public Ref exactRef(String name) throws IOException { + return reftableDatabase.exactRef(name); + } + + /** {@inheritDoc} */ + @Override + public List<Ref> getRefs() throws IOException { + return super.getRefs(); + } + + /** {@inheritDoc} */ + @Override + public Map<String, Ref> getRefs(String prefix) throws IOException { + List<Ref> refs = reftableDatabase.getRefsByPrefix(prefix); + RefList.Builder<Ref> builder = new RefList.Builder<>(refs.size()); + for (Ref r : refs) { + builder.add(r); + } + return new RefMap(prefix, builder.toRefList(), RefList.emptyList(), + RefList.emptyList()); + } + + /** {@inheritDoc} */ + @Override + public List<Ref> getAdditionalRefs() throws IOException { + return Collections.emptyList(); + } + + /** {@inheritDoc} */ + @Override + public Ref peel(Ref ref) throws IOException { + Ref oldLeaf = ref.getLeaf(); + if (oldLeaf.isPeeled() || oldLeaf.getObjectId() == null) { + return ref; + } + return recreate(ref, doPeel(oldLeaf), hasVersioning()); + + } + + private Ref doPeel(Ref leaf) throws IOException { + try (RevWalk rw = new RevWalk(fileRepository)) { + RevObject obj = rw.parseAny(leaf.getObjectId()); + if (obj instanceof RevTag) { + return new ObjectIdRef.PeeledTag(leaf.getStorage(), + leaf.getName(), leaf.getObjectId(), rw.peel(obj).copy(), + hasVersioning() ? leaf.getUpdateIndex() + : UNDEFINED_UPDATE_INDEX); + } + return new ObjectIdRef.PeeledNonTag(leaf.getStorage(), + leaf.getName(), leaf.getObjectId(), + hasVersioning() ? leaf.getUpdateIndex() + : UNDEFINED_UPDATE_INDEX); + + } + } + + private static Ref recreate(Ref old, Ref leaf, boolean hasVersioning) { + if (old.isSymbolic()) { + Ref dst = recreate(old.getTarget(), leaf, hasVersioning); + return new SymbolicRef(old.getName(), dst, + hasVersioning ? old.getUpdateIndex() + : UNDEFINED_UPDATE_INDEX); + } + return leaf; + } + + private class FileRefRename extends RefRename { + FileRefRename(RefUpdate src, RefUpdate dst) { + super(src, dst); + } + + void writeRename(ReftableWriter w) throws IOException { + long idx = reftableDatabase.nextUpdateIndex(); + w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin(); + List<Ref> refs = new ArrayList<>(3); + + Ref dest = destination.getRef(); + Ref head = exactRef(Constants.HEAD); + if (head != null && head.isSymbolic() + && head.getLeaf().getName().equals(source.getName())) { + head = new SymbolicRef(Constants.HEAD, dest, idx); + refs.add(head); + } + + ObjectId objId = source.getRef().getObjectId(); + + // XXX should we check if the source is a Tag vs. NonTag? + refs.add(new ObjectIdRef.PeeledNonTag(Ref.Storage.NEW, + destination.getName(), objId)); + refs.add(new ObjectIdRef.Unpeeled(Ref.Storage.NEW, source.getName(), + null)); + + w.sortAndWriteRefs(refs); + PersonIdent who = destination.getRefLogIdent(); + if (who == null) { + who = new PersonIdent(fileRepository); + } + + if (!destination.getRefLogMessage().isEmpty()) { + List<String> refnames = refs.stream().map(r -> r.getName()) + .collect(Collectors.toList()); + Collections.sort(refnames); + for (String s : refnames) { + ObjectId old = (Constants.HEAD.equals(s) + || s.equals(source.getName())) ? objId + : ObjectId.zeroId(); + ObjectId newId = (Constants.HEAD.equals(s) + || s.equals(destination.getName())) ? objId + : ObjectId.zeroId(); + + w.writeLog(s, idx, who, old, newId, + destination.getRefLogMessage()); + } + } + } + + @Override + protected RefUpdate.Result doRename() throws IOException { + Ref src = exactRef(source.getName()); + if (exactRef(destination.getName()) != null || src == null + || !source.getOldObjectId().equals(src.getObjectId())) { + return RefUpdate.Result.LOCK_FAILURE; + } + + if (src.isSymbolic()) { + // We could support this, but this is easier and compatible. + return RefUpdate.Result.IO_FAILURE; + } + + if (!addReftable(this::writeRename)) { + return RefUpdate.Result.LOCK_FAILURE; + } + + return RefUpdate.Result.RENAMED; + } + } + + /** {@inheritDoc} */ + @Override + public RefRename newRename(String fromName, String toName) + throws IOException { + RefUpdate src = newUpdate(fromName, true); + RefUpdate dst = newUpdate(toName, true); + return new FileRefRename(src, dst); + } + + /** {@inheritDoc} */ + @Override + public boolean isNameConflicting(String name) throws IOException { + return reftableDatabase.isNameConflicting(name, new TreeSet<>(), + new HashSet<>()); + } + + /** {@inheritDoc} */ + @Override + public void close() { + reftableStack.close(); + } + + /** {@inheritDoc} */ + @Override + public void create() throws IOException { + FileUtils.mkdir( + new File(fileRepository.getDirectory(), Constants.REFTABLE), + true); + } + + private boolean addReftable(FileReftableStack.Writer w) throws IOException { + if (!reftableStack.addReftable(w)) { + reftableStack.reload(); + reftableDatabase.clearCache(); + return false; + } + reftableDatabase.clearCache(); + + return true; + } + + private class FileReftableBatchRefUpdate extends ReftableBatchRefUpdate { + FileReftableBatchRefUpdate(FileReftableDatabase db, + Repository repository) { + super(db, db.reftableDatabase, db.getLock(), repository); + } + + @Override + protected void applyUpdates(List<Ref> newRefs, + List<ReceiveCommand> pending) throws IOException { + if (!addReftable(rw -> write(rw, newRefs, pending))) { + for (ReceiveCommand c : pending) { + if (c.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) { + c.setResult(RefUpdate.Result.LOCK_FAILURE); + } + } + } + } + } + + private class FileReftableRefUpdate extends RefUpdate { + FileReftableRefUpdate(Ref ref) { + super(ref); + } + + @Override + protected RefDatabase getRefDatabase() { + return FileReftableDatabase.this; + } + + @Override + protected Repository getRepository() { + return FileReftableDatabase.this.fileRepository; + } + + @Override + protected void unlock() { + // nop. + } + + private RevWalk rw; + + private Ref dstRef; + + @Override + public Result update(RevWalk walk) throws IOException { + try { + rw = walk; + return super.update(walk); + } finally { + rw = null; + } + } + + @Override + protected boolean tryLock(boolean deref) throws IOException { + dstRef = getRef(); + if (deref) { + dstRef = dstRef.getLeaf(); + } + + Ref derefed = exactRef(dstRef.getName()); + if (derefed != null) { + setOldObjectId(derefed.getObjectId()); + } + + return true; + } + + void writeUpdate(ReftableWriter w) throws IOException { + Ref newRef = null; + if (rw != null && !ObjectId.zeroId().equals(getNewObjectId())) { + RevObject obj = rw.parseAny(getNewObjectId()); + if (obj instanceof RevTag) { + newRef = new ObjectIdRef.PeeledTag(Ref.Storage.PACKED, + dstRef.getName(), getNewObjectId(), + rw.peel(obj).copy()); + } + } + if (newRef == null) { + newRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.PACKED, + dstRef.getName(), getNewObjectId()); + } + + long idx = reftableDatabase.nextUpdateIndex(); + w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin() + .writeRef(newRef); + + ObjectId oldId = getOldObjectId(); + if (oldId == null) { + oldId = ObjectId.zeroId(); + } + w.writeLog(dstRef.getName(), idx, getRefLogIdent(), oldId, + getNewObjectId(), getRefLogMessage()); + } + + @Override + public PersonIdent getRefLogIdent() { + PersonIdent who = super.getRefLogIdent(); + if (who == null) { + who = new PersonIdent(getRepository()); + } + return who; + } + + void writeDelete(ReftableWriter w) throws IOException { + Ref newRef = new ObjectIdRef.Unpeeled(Ref.Storage.NEW, + dstRef.getName(), null); + long idx = reftableDatabase.nextUpdateIndex(); + w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin() + .writeRef(newRef); + + ObjectId oldId = ObjectId.zeroId(); + Ref old = exactRef(dstRef.getName()); + if (old != null) { + old = old.getLeaf(); + if (old.getObjectId() != null) { + oldId = old.getObjectId(); + } + } + + w.writeLog(dstRef.getName(), idx, getRefLogIdent(), oldId, + ObjectId.zeroId(), getRefLogMessage()); + } + + @Override + protected Result doUpdate(Result desiredResult) throws IOException { + if (isRefLogIncludingResult()) { + setRefLogMessage( + getRefLogMessage() + ": " + desiredResult.toString(), //$NON-NLS-1$ + false); + } + + if (!addReftable(this::writeUpdate)) { + return Result.LOCK_FAILURE; + } + + return desiredResult; + } + + @Override + protected Result doDelete(Result desiredResult) throws IOException { + + if (isRefLogIncludingResult()) { + setRefLogMessage( + getRefLogMessage() + ": " + desiredResult.toString(), //$NON-NLS-1$ + false); + } + + if (!addReftable(this::writeDelete)) { + return Result.LOCK_FAILURE; + } + + return desiredResult; + } + + void writeLink(ReftableWriter w) throws IOException { + long idx = reftableDatabase.nextUpdateIndex(); + w.setMinUpdateIndex(idx).setMaxUpdateIndex(idx).begin() + .writeRef(dstRef); + + ObjectId beforeId = ObjectId.zeroId(); + Ref before = exactRef(dstRef.getName()); + if (before != null) { + before = before.getLeaf(); + if (before.getObjectId() != null) { + beforeId = before.getObjectId(); + } + } + + Ref after = dstRef.getLeaf(); + ObjectId afterId = ObjectId.zeroId(); + if (after.getObjectId() != null) { + afterId = after.getObjectId(); + } + + w.writeLog(dstRef.getName(), idx, getRefLogIdent(), beforeId, + afterId, getRefLogMessage()); + } + + @Override + protected Result doLink(String target) throws IOException { + if (isRefLogIncludingResult()) { + setRefLogMessage( + getRefLogMessage() + ": " + Result.FORCED.toString(), //$NON-NLS-1$ + false); + } + + boolean exists = exactRef(getName()) != null; + dstRef = new SymbolicRef(getName(), + new ObjectIdRef.Unpeeled(Ref.Storage.NEW, target, null), + reftableDatabase.nextUpdateIndex()); + + if (!addReftable(this::writeLink)) { + return Result.LOCK_FAILURE; + } + // XXX unclear if we should support FORCED here. Baseclass says + // NEW is OK ? + return exists ? Result.FORCED : Result.NEW; + } + } + + private static void writeConvertTable(Repository repo, ReftableWriter w, + boolean writeLogs) throws IOException { + int size = 0; + List<Ref> refs = repo.getRefDatabase().getRefs(); + if (writeLogs) { + for (Ref r : refs) { + ReflogReader rlr = repo.getReflogReader(r.getName()); + if (rlr != null) { + size = Math.max(rlr.getReverseEntries().size(), size); + } + } + } + // We must use 1 here, nextUpdateIndex() on the empty stack is 1. + w.setMinUpdateIndex(1).setMaxUpdateIndex(size + 1).begin(); + + // The spec says to write the logs in the first table, and put refs in a + // separate table, but this complicates the compaction (when we can we drop + // deletions? Can we compact the .log table and the .ref table together?) + try (RevWalk rw = new RevWalk(repo)) { + List<Ref> toWrite = new ArrayList<>(refs.size()); + for (Ref r : refs) { + toWrite.add(refForWrite(rw, r)); + } + w.sortAndWriteRefs(toWrite); + } + + if (writeLogs) { + for (Ref r : refs) { + long idx = size; + ReflogReader reader = repo.getReflogReader(r.getName()); + if (reader == null) { + continue; + } + for (ReflogEntry e : reader.getReverseEntries()) { + w.writeLog(r.getName(), idx, e.getWho(), e.getOldId(), + e.getNewId(), e.getComment()); + idx--; + } + } + } + } + + private static Ref refForWrite(RevWalk rw, Ref r) throws IOException { + if (r.isSymbolic()) { + return new SymbolicRef(r.getName(), new ObjectIdRef.Unpeeled(NEW, + r.getTarget().getName(), null)); + } + ObjectId newId = r.getObjectId(); + RevObject peel = null; + try { + RevObject obj = rw.parseAny(newId); + if (obj instanceof RevTag) { + peel = rw.peel(obj); + } + } catch (MissingObjectException e) { + /* ignore this error and copy the dangling object ID into reftable too. */ + } + if (peel != null) { + return new ObjectIdRef.PeeledTag(PACKED, r.getName(), newId, + peel.copy()); + } + + return new ObjectIdRef.PeeledNonTag(PACKED, r.getName(), newId); + } + + /** + * @param repo + * the repository + * @param writeLogs + * whether to write reflogs + * @return a reftable based RefDB from an existing repository. + * @throws IOException + * on IO error + */ + public static FileReftableDatabase convertFrom(FileRepository repo, + boolean writeLogs) throws IOException { + FileReftableDatabase newDb = null; + File reftableList = null; + try { + File reftableDir = new File(repo.getDirectory(), + Constants.REFTABLE); + reftableList = new File(reftableDir, Constants.TABLES_LIST); + if (!reftableDir.isDirectory()) { + reftableDir.mkdir(); + } + + try (FileReftableStack stack = new FileReftableStack(reftableList, + reftableDir, null, () -> repo.getConfig())) { + stack.addReftable(rw -> writeConvertTable(repo, rw, writeLogs)); + } + reftableList = null; + } finally { + if (reftableList != null) { + reftableList.delete(); + } + } + return newDb; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java new file mode 100644 index 0000000000..68d82c20d2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java @@ -0,0 +1,766 @@ +/* + * Copyright (C) 2019 Google LLC + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.file; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.errors.LockFailedException; +import org.eclipse.jgit.internal.storage.io.BlockSource; +import org.eclipse.jgit.internal.storage.reftable.MergedReftable; +import org.eclipse.jgit.internal.storage.reftable.ReftableCompactor; +import org.eclipse.jgit.internal.storage.reftable.ReftableConfig; +import org.eclipse.jgit.internal.storage.reftable.ReftableReader; +import org.eclipse.jgit.internal.storage.reftable.ReftableWriter; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.util.FileUtils; + +/** + * A mutable stack of reftables on local filesystem storage. Not thread-safe. + * This is an AutoCloseable because this object owns the file handles to the + * open reftables. + */ +public class FileReftableStack implements AutoCloseable { + private static class StackEntry { + + String name; + + ReftableReader reftableReader; + } + + private MergedReftable mergedReftable; + + private List<StackEntry> stack; + + private long lastNextUpdateIndex; + + private final File stackPath; + + private final File reftableDir; + + private final Runnable onChange; + + private final Supplier<Config> configSupplier; + + // Used for stats & testing. + static class CompactionStats { + + long tables; + + long bytes; + + int attempted; + + int failed; + + long refCount; + + long logCount; + + CompactionStats() { + tables = 0; + bytes = 0; + attempted = 0; + failed = 0; + logCount = 0; + refCount = 0; + } + } + + private final CompactionStats stats; + + /** + * Creates a stack corresponding to the list of reftables in the argument + * + * @param stackPath + * the filename for the stack. + * @param reftableDir + * the dir holding the tables. + * @param onChange + * hook to call if we notice a new write + * @param configSupplier + * Config supplier + * @throws IOException + * on I/O problems + */ + public FileReftableStack(File stackPath, File reftableDir, + @Nullable Runnable onChange, Supplier<Config> configSupplier) + throws IOException { + this.stackPath = stackPath; + this.reftableDir = reftableDir; + this.stack = new ArrayList<>(); + this.configSupplier = configSupplier; + this.onChange = onChange; + + // skip event notification + lastNextUpdateIndex = 0; + reload(); + + stats = new CompactionStats(); + } + + CompactionStats getStats() { + return stats; + } + + /** Thrown if the update indices in the stack are not monotonic */ + public static class ReftableNumbersNotIncreasingException + extends RuntimeException { + private static final long serialVersionUID = 1L; + + String name; + + long lastMax; + + long min; + + ReftableNumbersNotIncreasingException(String name, long lastMax, + long min) { + this.name = name; + this.lastMax = lastMax; + this.min = min; + } + } + + /** + * Reloads the stack, potentially reusing opened reftableReaders. + * + * @param names + * holds the names of the tables to load. + * @throws FileNotFoundException + * load must be retried. + * @throws IOException + * on other IO errors. + */ + private void reloadOnce(List<String> names) + throws IOException, FileNotFoundException { + Map<String, ReftableReader> current = stack.stream() + .collect(Collectors.toMap(e -> e.name, e -> e.reftableReader)); + + List<ReftableReader> newTables = new ArrayList<>(); + List<StackEntry> newStack = new ArrayList<>(stack.size() + 1); + try { + ReftableReader last = null; + for (String name : names) { + StackEntry entry = new StackEntry(); + entry.name = name; + + ReftableReader t = null; + if (current.containsKey(name)) { + t = current.remove(name); + } else { + File subtable = new File(reftableDir, name); + FileInputStream is; + + is = new FileInputStream(subtable); + + t = new ReftableReader(BlockSource.from(is)); + newTables.add(t); + } + + if (last != null) { + // TODO: move this to MergedReftable + if (last.maxUpdateIndex() >= t.minUpdateIndex()) { + throw new ReftableNumbersNotIncreasingException(name, + last.maxUpdateIndex(), t.minUpdateIndex()); + } + } + last = t; + + entry.reftableReader = t; + newStack.add(entry); + } + // survived without exceptions: swap in new stack, and close + // dangling tables. + stack = newStack; + newTables.clear(); + + current.values().forEach(r -> { + try { + r.close(); + } catch (IOException e) { + throw new AssertionError(e); + } + }); + } finally { + newTables.forEach(t -> { + try { + t.close(); + } catch (IOException ioe) { + // reader close should not generate errors. + throw new AssertionError(ioe); + } + }); + } + } + + void reload() throws IOException { + // Try for 2.5 seconds. + long deadline = System.currentTimeMillis() + 2500; + // A successful reftable transaction is 2 atomic file writes + // (open, write, close, rename), which a fast Linux system should be + // able to do in about ~200us. So 1 ms should be ample time. + long min = 1; + long max = 1000; + long delay = 0; + boolean success = false; + + // Don't check deadline for the first 3 retries, so we can step with a + // debugger without worrying about deadlines. + int tries = 0; + while (tries < 3 || System.currentTimeMillis() < deadline) { + List<String> names = readTableNames(); + tries++; + try { + reloadOnce(names); + success = true; + break; + } catch (FileNotFoundException e) { + List<String> changed = readTableNames(); + if (changed.equals(names)) { + throw e; + } + } + + delay = FileUtils.delay(delay, min, max); + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + + if (!success) { + throw new LockFailedException(stackPath); + } + + mergedReftable = new MergedReftable(stack.stream() + .map(x -> x.reftableReader).collect(Collectors.toList())); + long curr = nextUpdateIndex(); + if (lastNextUpdateIndex > 0 && lastNextUpdateIndex != curr + && onChange != null) { + onChange.run(); + } + lastNextUpdateIndex = curr; + } + + /** + * @return the merged reftable + */ + public MergedReftable getMergedReftable() { + return mergedReftable; + } + + /** + * Writer is a callable that writes data to a reftable under construction. + * It should set the min/max update index, and then write refs and/or logs. + * It should not call finish() on the writer. + */ + public interface Writer { + /** + * Write data to reftable + * + * @param w + * writer to use + * @throws IOException + */ + void call(ReftableWriter w) throws IOException; + } + + private List<String> readTableNames() throws IOException { + List<String> names = new ArrayList<>(stack.size() + 1); + + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(stackPath), UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (!line.isEmpty()) { + names.add(line); + } + } + } catch (FileNotFoundException e) { + // file isn't there: empty repository. + } + return names; + } + + /** + * @return true if the on-disk file corresponds to the in-memory data. + * @throws IOException + * on IO problem + */ + boolean isUpToDate() throws IOException { + // We could use FileSnapshot to avoid reading the file, but the file is + // small so it's probably a minor optimization. + try { + List<String> names = readTableNames(); + if (names.size() != stack.size()) { + return false; + } + for (int i = 0; i < names.size(); i++) { + if (!names.get(i).equals(stack.get(i).name)) { + return false; + } + } + } catch (FileNotFoundException e) { + return stack.isEmpty(); + } + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() { + for (StackEntry entry : stack) { + try { + entry.reftableReader.close(); + } catch (Exception e) { + // we are reading; this should never fail. + throw new AssertionError(e); + } + } + } + + private long nextUpdateIndex() throws IOException { + return stack.size() > 0 + ? stack.get(stack.size() - 1).reftableReader.maxUpdateIndex() + + 1 + : 1; + } + + private String filename(long low, long high) { + return String.format("%012x-%012x", //$NON-NLS-1$ + Long.valueOf(low), Long.valueOf(high)); + } + + /** + * Tries to add a new reftable to the stack. Returns true if it succeeded, + * or false if there was a lock failure, due to races with other processes. + * This is package private so FileReftableDatabase can call into here. + * + * @param w + * writer to write data to a reftable under construction + * @return true if the transaction was successful. + * @throws IOException + * on I/O problems + */ + @SuppressWarnings("nls") + public boolean addReftable(Writer w) throws IOException { + LockFile lock = new LockFile(stackPath); + try { + if (!lock.lockForAppend()) { + return false; + } + if (!isUpToDate()) { + return false; + } + + String fn = filename(nextUpdateIndex(), nextUpdateIndex()); + + File tmpTable = File.createTempFile(fn + "_", ".ref", + stackPath.getParentFile()); + + ReftableWriter.Stats s; + try (FileOutputStream fos = new FileOutputStream(tmpTable)) { + ReftableWriter rw = new ReftableWriter(reftableConfig(), fos); + w.call(rw); + rw.finish(); + s = rw.getStats(); + } + + if (s.minUpdateIndex() < nextUpdateIndex()) { + return false; + } + + // The spec says to name log-only files with .log, which is somewhat + // pointless given compaction, but we do so anyway. + fn += s.refCount() > 0 ? ".ref" : ".log"; + File dest = new File(reftableDir, fn); + + FileUtils.rename(tmpTable, dest, StandardCopyOption.ATOMIC_MOVE); + lock.write((fn + "\n").getBytes(UTF_8)); + if (!lock.commit()) { + FileUtils.delete(dest); + return false; + } + + reload(); + + autoCompact(); + } finally { + lock.unlock(); + } + return true; + } + + private ReftableConfig reftableConfig() { + return new ReftableConfig(configSupplier.get()); + } + + /** + * Write the reftable for the given range into a temp file. + * + * @param first + * index of first stack entry to be written + * @param last + * index of last stack entry to be written + * @return the file holding the replacement table. + * @throws IOException + * on I/O problem + */ + private File compactLocked(int first, int last) throws IOException { + String fn = filename(first, last); + + File tmpTable = File.createTempFile(fn + "_", ".ref", //$NON-NLS-1$//$NON-NLS-2$ + stackPath.getParentFile()); + try (FileOutputStream fos = new FileOutputStream(tmpTable)) { + ReftableCompactor c = new ReftableCompactor(fos) + .setConfig(reftableConfig()) + .setIncludeDeletes(first > 0); + + List<ReftableReader> compactMe = new ArrayList<>(); + long totalBytes = 0; + for (int i = first; i <= last; i++) { + compactMe.add(stack.get(i).reftableReader); + totalBytes += stack.get(i).reftableReader.size(); + } + c.addAll(compactMe); + + c.compact(); + + // Even though the compaction did not definitely succeed, we keep + // tally here as we've expended the effort. + stats.bytes += totalBytes; + stats.tables += first - last + 1; + stats.attempted++; + stats.refCount += c.getStats().refCount(); + stats.logCount += c.getStats().logCount(); + } + + return tmpTable; + } + + /** + * Compacts a range of the stack, following the file locking protocol + * documented in the spec. + * + * @param first + * index of first stack entry to be considered in compaction + * @param last + * index of last stack entry to be considered in compaction + * @return true if a compaction was successfully applied. + * @throws IOException + * on I/O problem + */ + boolean compactRange(int first, int last) throws IOException { + if (first >= last) { + return true; + } + LockFile lock = new LockFile(stackPath); + + File tmpTable = null; + List<LockFile> subtableLocks = new ArrayList<>(); + + try { + if (!lock.lock()) { + return false; + } + if (!isUpToDate()) { + return false; + } + + List<File> deleteOnSuccess = new ArrayList<>(); + for (int i = first; i <= last; i++) { + File f = new File(reftableDir, stack.get(i).name); + LockFile lf = new LockFile(f); + if (!lf.lock()) { + return false; + } + subtableLocks.add(lf); + deleteOnSuccess.add(f); + } + + lock.unlock(); + lock = null; + + tmpTable = compactLocked(first, last); + + lock = new LockFile(stackPath); + if (!lock.lock()) { + return false; + } + if (!isUpToDate()) { + return false; + } + + String fn = filename( + stack.get(first).reftableReader.minUpdateIndex(), + stack.get(last).reftableReader.maxUpdateIndex()); + + // The spec suggests to use .log for log-only tables, and collect + // all log entries in a single file at the bottom of the stack. That would + // require supporting overlapping ranges for the different tables. For the + // sake of simplicity, we simply ignore this and always produce a log + + // ref combined table. + fn += ".ref"; //$NON-NLS-1$ + File dest = new File(reftableDir, fn); + + FileUtils.rename(tmpTable, dest, StandardCopyOption.ATOMIC_MOVE); + tmpTable = null; + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < first; i++) { + sb.append(stack.get(i).name + "\n"); //$NON-NLS-1$ + } + sb.append(fn + "\n"); //$NON-NLS-1$ + for (int i = last + 1; i < stack.size(); i++) { + sb.append(stack.get(i).name + "\n"); //$NON-NLS-1$ + } + + lock.write(sb.toString().getBytes(UTF_8)); + if (!lock.commit()) { + dest.delete(); + return false; + } + + for (File f : deleteOnSuccess) { + Files.delete(f.toPath()); + } + + reload(); + return true; + } finally { + if (tmpTable != null) { + tmpTable.delete(); + } + for (LockFile lf : subtableLocks) { + lf.unlock(); + } + if (lock != null) { + lock.unlock(); + } + } + } + + /** + * Calculate an approximate log2. + * + * @param sz + * @return log2 + */ + static int log(long sz) { + long base = 2; + if (sz <= 0) { + throw new IllegalArgumentException("log2 negative"); //$NON-NLS-1$ + } + int l = 0; + while (sz > 0) { + l++; + sz /= base; + } + + return l - 1; + } + + /** + * A segment is a consecutive list of reftables of the same approximate + * size. + */ + static class Segment { + // the approximate log_2 of the size. + int log; + + // The total bytes in this segment + long bytes; + + int start; + + int end; // exclusive. + + int size() { + return end - start; + } + + Segment(int start, int end, int log, long bytes) { + this.log = log; + this.start = start; + this.end = end; + this.bytes = bytes; + } + + Segment() { + this(0, 0, 0, 0); + } + + @Override + public int hashCode() { + return 0; // appease error-prone + } + + @Override + public boolean equals(Object other) { + Segment o = (Segment) other; + return o.bytes == bytes && o.log == log && o.start == start + && o.end == end; + } + + @SuppressWarnings("boxing") + @Override + public String toString() { + return String.format("{ [%d,%d) l=%d sz=%d }", start, end, log, //$NON-NLS-1$ + bytes); + } + } + + static List<Segment> segmentSizes(long[] sizes) { + List<Segment> segments = new ArrayList<>(); + Segment cur = new Segment(); + for (int i = 0; i < sizes.length; i++) { + int l = log(sizes[i]); + if (l != cur.log && cur.bytes > 0) { + segments.add(cur); + cur = new Segment(); + cur.start = i; + cur.log = l; + } + + cur.log = l; + cur.end = i + 1; + cur.bytes += sizes[i]; + } + segments.add(cur); + return segments; + } + + private static Optional<Segment> autoCompactCandidate(long[] sizes) { + if (sizes.length == 0) { + return Optional.empty(); + } + + // The cost of compaction is proportional to the size, and we want to + // avoid frequent large compactions. We do this by playing the game 2048 + // here: first compact together the smallest tables if there are more + // than one. Then try to see if the result will be big enough to match + // up with next up. + + List<Segment> segments = segmentSizes(sizes); + segments = segments.stream().filter(s -> s.size() > 1) + .collect(Collectors.toList()); + if (segments.isEmpty()) { + return Optional.empty(); + } + + Optional<Segment> optMinSeg = segments.stream() + .min(Comparator.comparing(s -> Integer.valueOf(s.log))); + // Input is non-empty, so always present. + Segment smallCollected = optMinSeg.get(); + while (smallCollected.start > 0) { + int prev = smallCollected.start - 1; + long prevSize = sizes[prev]; + if (log(smallCollected.bytes) < log(prevSize)) { + break; + } + smallCollected.start = prev; + smallCollected.bytes += prevSize; + } + + return Optional.of(smallCollected); + } + + /** + * Heuristically tries to compact the stack if the stack has a suitable + * shape. + * + * @throws IOException + */ + private void autoCompact() throws IOException { + Optional<Segment> cand = autoCompactCandidate(tableSizes()); + if (cand.isPresent()) { + if (!compactRange(cand.get().start, cand.get().end - 1)) { + stats.failed++; + } + } + } + + // 68b footer, 24b header = 92. + private static long OVERHEAD = 91; + + private long[] tableSizes() throws IOException { + long[] sizes = new long[stack.size()]; + for (int i = 0; i < stack.size(); i++) { + // If we don't subtract the overhead, the file size isn't + // proportional to the number of entries. This will cause us to + // compact too often, which is expensive. + sizes[i] = stack.get(i).reftableReader.size() - OVERHEAD; + } + return sizes; + } + + void compactFully() throws IOException { + if (!compactRange(0, stack.size() - 1)) { + stats.failed++; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java index 4f5f8a613e..9929c46af6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java @@ -46,13 +46,20 @@ package org.eclipse.jgit.internal.storage.file; +import static java.util.stream.Collectors.toList; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.text.MessageFormat; import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; @@ -68,21 +75,26 @@ import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateHandle; import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateRepository; import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase; import org.eclipse.jgit.lib.BaseRepositoryBuilder; +import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig.HideDotFiles; import org.eclipse.jgit.lib.CoreConfig.SymLinks; +import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.ReflogReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.storage.pack.PackConfig; +import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; @@ -121,7 +133,7 @@ public class FileRepository extends Repository { private static final String UNNAMED = "Unnamed repository; edit this file to name it for gitweb."; //$NON-NLS-1$ private final FileBasedConfig repoConfig; - private final RefDatabase refs; + private RefDatabase refs; private final ObjectDirectory objectDatabase; private final Object snapshotLock = new Object(); @@ -197,9 +209,13 @@ public class FileRepository extends Repository { ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0); String reftype = repoConfig.getString( - "extensions", null, "refStorage"); //$NON-NLS-1$ //$NON-NLS-2$ + ConfigConstants.CONFIG_EXTENSIONS_SECTION, null, + ConfigConstants.CONFIG_KEY_REF_STORAGE); if (repositoryFormatVersion >= 1 && reftype != null) { - if (StringUtils.equalsIgnoreCase(reftype, "reftree")) { //$NON-NLS-1$ + if (StringUtils.equalsIgnoreCase(reftype, + ConfigConstants.CONFIG_REF_STORAGE_REFTABLE)) { + refs = new FileReftableDatabase(this); + } else if (StringUtils.equalsIgnoreCase(reftype, "reftree")) { //$NON-NLS-1$ refs = new RefTreeDatabase(this, new RefDirectory(this)); } else { throw new IOException(JGitText.get().unknownRepositoryFormat); @@ -358,9 +374,8 @@ public class FileRepository extends Repository { File directory = getDirectory(); if (directory != null) { return directory.getPath(); - } else { - throw new IllegalStateException(); } + throw new IllegalStateException(); } /** {@inheritDoc} */ @@ -531,10 +546,19 @@ public class FileRepository extends Repository { /** {@inheritDoc} */ @Override public ReflogReader getReflogReader(String refName) throws IOException { + if (refs instanceof FileReftableDatabase) { + // Cannot use findRef: reftable stores log data for deleted or renamed + // branches. + return ((FileReftableDatabase)refs).getReflogReader(refName); + } + + // TODO: use exactRef here, which offers more predictable and therefore preferable + // behavior. Ref ref = findRef(refName); - if (ref != null) - return new ReflogReaderImpl(this, ref.getName()); - return null; + if (ref == null) { + return null; + } + return new ReflogReaderImpl(this, ref.getName()); } /** {@inheritDoc} */ @@ -614,4 +638,218 @@ public class FileRepository extends Repository { throw new JGitInternalException(JGitText.get().gcFailed, e); } } + + /** + * Converts the RefDatabase from reftable to RefDirectory. This operation is + * not atomic. + * + * @param writeLogs + * whether to write reflogs + * @param backup + * whether to rename or delete the old storage files. If set to + * {@code true}, the reftable list is left in {@code refs.old}, + * and the {@code reftable/} dir is left alone. If set to + * {@code false}, the {@code reftable/} dir is removed, and + * {@code refs} file is removed. + * @throws IOException + * on IO problem + */ + void convertToPackedRefs(boolean writeLogs, boolean backup) throws IOException { + List<Ref> all = refs.getRefs(); + File packedRefs = new File(getDirectory(), Constants.PACKED_REFS); + if (packedRefs.exists()) { + throw new IOException(MessageFormat.format(JGitText.get().fileAlreadyExists, + packedRefs.getName())); + } + + File refsFile = new File(getDirectory(), "refs"); //$NON-NLS-1$ + File refsHeadsFile = new File(refsFile, "heads");//$NON-NLS-1$ + File headFile = new File(getDirectory(), Constants.HEAD); + FileReftableDatabase oldDb = (FileReftableDatabase) refs; + + // Remove the dummy files that ensure compatibility with older git + // versions (see convertToReftable). First make room for refs/heads/ + refsHeadsFile.delete(); + // RefDirectory wants to create the refs/ directory from scratch, so + // remove that too. + refsFile.delete(); + // remove HEAD so its previous invalid value doesn't cause issues. + headFile.delete(); + + // This is not atomic, but there is no way to instantiate a RefDirectory + // that is disconnected from the current repo. + RefDirectory refDir = new RefDirectory(this); + refs = refDir; + refs.create(); + + ReflogWriter logWriter = refDir.newLogWriter(true); + List<Ref> symrefs = new ArrayList<>(); + BatchRefUpdate bru = refs.newBatchUpdate(); + for (Ref r : all) { + if (r.isSymbolic()) { + symrefs.add(r); + } else { + bru.addCommand(new ReceiveCommand(ObjectId.zeroId(), + r.getObjectId(), r.getName())); + } + + if (writeLogs) { + List<ReflogEntry> logs = oldDb.getReflogReader(r.getName()) + .getReverseEntries(); + Collections.reverse(logs); + for (ReflogEntry e : logs) { + logWriter.log(r.getName(), e); + } + } + } + + try (RevWalk rw = new RevWalk(this)) { + bru.execute(rw, NullProgressMonitor.INSTANCE); + } + + List<String> failed = new ArrayList<>(); + for (ReceiveCommand cmd : bru.getCommands()) { + if (cmd.getResult() != ReceiveCommand.Result.OK) { + failed.add(cmd.getRefName() + ": " + cmd.getResult()); //$NON-NLS-1$ + } + } + + if (!failed.isEmpty()) { + throw new IOException(String.format("%s: %s", //$NON-NLS-1$ + JGitText.get().failedToConvert, + StringUtils.join(failed, ", "))); //$NON-NLS-1$ + } + + for (Ref s : symrefs) { + RefUpdate up = refs.newUpdate(s.getName(), false); + up.setForceUpdate(true); + RefUpdate.Result res = up.link(s.getTarget().getName()); + if (res != RefUpdate.Result.NEW + && res != RefUpdate.Result.NO_CHANGE) { + throw new IOException( + String.format("ref %s: %s", s.getName(), res)); //$NON-NLS-1$ + } + } + + if (!backup) { + File reftableDir = new File(getDirectory(), Constants.REFTABLE); + FileUtils.delete(reftableDir, + FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS); + } + repoConfig.unset(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null, + ConfigConstants.CONFIG_KEY_REF_STORAGE); + } + + /** + * Converts the RefDatabase from RefDirectory to reftable. This operation is + * not atomic. + * + * @param writeLogs + * whether to write reflogs + * @param backup + * whether to rename or delete the old storage files. If set to + * {@code true}, the loose refs are left in {@code refs.old}, the + * packed-refs in {@code packed-refs.old} and reflogs in + * {@code refs.old/}. HEAD is left in {@code HEAD.old} and also + * {@code .log} is appended to additional refs. If set to + * {@code false}, the {@code refs/} and {@code logs/} directories + * and {@code HEAD} and additional symbolic refs are removed. + * @throws IOException + * on IO problem + */ + @SuppressWarnings("nls") + void convertToReftable(boolean writeLogs, boolean backup) + throws IOException { + File reftableDir = new File(getDirectory(), Constants.REFTABLE); + File headFile = new File(getDirectory(), Constants.HEAD); + if (reftableDir.exists() && reftableDir.listFiles().length > 0) { + throw new IOException(JGitText.get().reftableDirExists); + } + + // Ignore return value, as it is tied to temporary newRefs file. + FileReftableDatabase.convertFrom(this, writeLogs); + + File refsFile = new File(getDirectory(), "refs"); + + // non-atomic: remove old data. + File packedRefs = new File(getDirectory(), Constants.PACKED_REFS); + File logsDir = new File(getDirectory(), Constants.LOGS); + + List<String> additional = getRefDatabase().getAdditionalRefs().stream() + .map(Ref::getName).collect(toList()); + additional.add(Constants.HEAD); + if (backup) { + FileUtils.rename(refsFile, new File(getDirectory(), "refs.old")); + if (packedRefs.exists()) { + FileUtils.rename(packedRefs, new File(getDirectory(), + Constants.PACKED_REFS + ".old")); + } + if (logsDir.exists()) { + FileUtils.rename(logsDir, + new File(getDirectory(), Constants.LOGS + ".old")); + } + for (String r : additional) { + FileUtils.rename(new File(getDirectory(), r), + new File(getDirectory(), r + ".old")); + } + } else { + FileUtils.delete(packedRefs, FileUtils.SKIP_MISSING); + FileUtils.delete(headFile); + FileUtils.delete(logsDir, FileUtils.RECURSIVE); + FileUtils.delete(refsFile, FileUtils.RECURSIVE); + for (String r : additional) { + new File(getDirectory(), r).delete(); + } + } + + FileUtils.mkdir(refsFile, true); + + // By putting in a dummy HEAD, old versions of Git still detect a repo + // (that they can't read) + try (OutputStream os = new FileOutputStream(headFile)) { + os.write(Constants.encodeASCII("ref: refs/heads/.invalid")); + } + + // Some tools might write directly into .git/refs/heads/BRANCH. By + // putting a file here, this fails spectacularly. + FileUtils.createNewFile(new File(refsFile, "heads")); + + repoConfig.setString(ConfigConstants.CONFIG_EXTENSIONS_SECTION, null, + ConfigConstants.CONFIG_KEY_REF_STORAGE, + ConfigConstants.CONFIG_REF_STORAGE_REFTABLE); + repoConfig.setLong(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 1); + repoConfig.save(); + refs.close(); + refs = new FileReftableDatabase(this); + } + + /** + * Converts between ref storage formats. + * + * @param format + * the format to convert to, either "reftable" or "refdir" + * @param writeLogs + * whether to write reflogs + * @param backup + * whether to make a backup of the old data + * @throws IOException + * on I/O problems. + */ + @SuppressWarnings("nls") + public void convertRefStorage(String format, boolean writeLogs, + boolean backup) throws IOException { + if (format.equals("reftable")) { //$NON-NLS-1$ + if (refs instanceof RefDirectory) { + convertToReftable(writeLogs, backup); + } + } else if (format.equals("refdir")) {//$NON-NLS-1$ + if (refs instanceof FileReftableDatabase) { + convertToPackedRefs(writeLogs, backup); + } + } else { + throw new IOException(String.format( + "unknown supported ref storage format '%s'", format)); + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java index 0ff3f615bf..5edcf37420 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java @@ -131,7 +131,7 @@ import org.slf4j.LoggerFactory; * adapted to FileRepositories. */ public class GC { - private final static Logger LOG = LoggerFactory + private static final Logger LOG = LoggerFactory .getLogger(GC.class); private static final String PRUNE_EXPIRE_DEFAULT = "2.weeks.ago"; //$NON-NLS-1$ @@ -773,13 +773,26 @@ public class GC { } /** - * Packs all non-symbolic, loose refs into packed-refs. + * Pack ref storage. For a RefDirectory database, this packs all + * non-symbolic, loose refs into packed-refs. For Reftable, all of the data + * is compacted into a single table. * * @throws java.io.IOException */ public void packRefs() throws IOException { - Collection<Ref> refs = repo.getRefDatabase() - .getRefsByPrefix(Constants.R_REFS); + RefDatabase refDb = repo.getRefDatabase(); + if (refDb instanceof FileReftableDatabase) { + // TODO: abstract this more cleanly. + pm.beginTask(JGitText.get().packRefs, 1); + try { + ((FileReftableDatabase) refDb).compactFully(); + } finally { + pm.endTask(); + } + return; + } + + Collection<Ref> refs = refDb.getRefsByPrefix(Constants.R_REFS); List<String> refsToBePacked = new ArrayList<>(refs.size()); pm.beginTask(JGitText.get().packRefs, refs.size()); try { @@ -897,7 +910,10 @@ public class GC { throw new IOException(e); } prunePacked(); - deleteEmptyRefsFolders(); + if (repo.getRefDatabase() instanceof RefDirectory) { + // TODO: abstract this more cleanly. + deleteEmptyRefsFolders(); + } deleteOrphans(); deleteTempPacksIdx(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java index 82458c1acf..13b9e79be8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java @@ -152,11 +152,10 @@ class GcLog { boolean commit() { if (nonEmpty) { return lock.commit(); - } else { - logFile.delete(); - lock.unlock(); - return true; } + logFile.delete(); + lock.unlock(); + return true; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java index 6af41256d6..78262e9773 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java @@ -1,45 +1,12 @@ /* * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2006-2021, Shawn O. Pearce <spearce@spearce.org> and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.storage.file; @@ -83,7 +50,7 @@ import org.slf4j.LoggerFactory; * name. */ public class LockFile { - private final static Logger LOG = LoggerFactory.getLogger(LockFile.class); + private static final Logger LOG = LoggerFactory.getLogger(LockFile.class); /** * Unlock the given file. @@ -129,11 +96,15 @@ public class LockFile { private boolean haveLck; - FileOutputStream os; + private FileOutputStream os; private boolean needSnapshot; - boolean fsync; + private boolean fsync; + + private boolean isAppend; + + private boolean written; private FileSnapshot commitSnapshot; @@ -160,6 +131,10 @@ public class LockFile { * does not hold the lock. */ public boolean lock() throws IOException { + if (haveLck) { + throw new IllegalStateException( + MessageFormat.format(JGitText.get().lockAlreadyHeld, ref)); + } FileUtils.mkdirs(lck.getParentFile(), true); try { token = FS.DETECTED.createNewFileAtomic(lck); @@ -167,18 +142,15 @@ public class LockFile { LOG.error(JGitText.get().failedCreateLockFile, lck, e); throw e; } - if (token.isCreated()) { + boolean obtainedLock = token.isCreated(); + if (obtainedLock) { haveLck = true; - try { - os = new FileOutputStream(lck); - } catch (IOException ioe) { - unlock(); - throw ioe; - } + isAppend = false; + written = false; } else { closeToken(); } - return haveLck; + return obtainedLock; } /** @@ -191,12 +163,24 @@ public class LockFile { * does not hold the lock. */ public boolean lockForAppend() throws IOException { - if (!lock()) + if (!lock()) { return false; + } copyCurrentContent(); + isAppend = true; + written = false; return true; } + // For tests only + boolean isLocked() { + return haveLck; + } + + private FileOutputStream getStream() throws IOException { + return new FileOutputStream(lck, isAppend); + } + /** * Copy the current file content into the temporary file. * <p> @@ -218,32 +202,31 @@ public class LockFile { */ public void copyCurrentContent() throws IOException { requireLock(); - try { + try (FileOutputStream out = getStream()) { try (FileInputStream fis = new FileInputStream(ref)) { if (fsync) { FileChannel in = fis.getChannel(); long pos = 0; long cnt = in.size(); while (0 < cnt) { - long r = os.getChannel().transferFrom(in, pos, cnt); + long r = out.getChannel().transferFrom(in, pos, cnt); pos += r; cnt -= r; } } else { final byte[] buf = new byte[2048]; int r; - while ((r = fis.read(buf)) >= 0) - os.write(buf, 0, r); + while ((r = fis.read(buf)) >= 0) { + out.write(buf, 0, r); + } } + } catch (FileNotFoundException fnfe) { + if (ref.exists()) { + throw fnfe; + } + // Don't worry about a file that doesn't exist yet, it + // conceptually has no current content to copy. } - } catch (FileNotFoundException fnfe) { - if (ref.exists()) { - unlock(); - throw fnfe; - } - // Don't worry about a file that doesn't exist yet, it - // conceptually has no current content to copy. - // } catch (IOException | RuntimeException | Error ioe) { unlock(); throw ioe; @@ -286,18 +269,22 @@ public class LockFile { */ public void write(byte[] content) throws IOException { requireLock(); - try { + try (FileOutputStream out = getStream()) { + if (written) { + throw new IOException(MessageFormat + .format(JGitText.get().lockStreamClosed, ref)); + } if (fsync) { - FileChannel fc = os.getChannel(); + FileChannel fc = out.getChannel(); ByteBuffer buf = ByteBuffer.wrap(content); - while (0 < buf.remaining()) + while (0 < buf.remaining()) { fc.write(buf); + } fc.force(true); } else { - os.write(content); + out.write(content); } - os.close(); - os = null; + written = true; } catch (IOException | RuntimeException | Error ioe) { unlock(); throw ioe; @@ -316,36 +303,67 @@ public class LockFile { public OutputStream getOutputStream() { requireLock(); - final OutputStream out; - if (fsync) - out = Channels.newOutputStream(os.getChannel()); - else - out = os; + if (written || os != null) { + throw new IllegalStateException(MessageFormat + .format(JGitText.get().lockStreamMultiple, ref)); + } return new OutputStream() { + + private OutputStream out; + + private boolean closed; + + private OutputStream get() throws IOException { + if (written) { + throw new IOException(MessageFormat + .format(JGitText.get().lockStreamMultiple, ref)); + } + if (out == null) { + os = getStream(); + if (fsync) { + out = Channels.newOutputStream(os.getChannel()); + } else { + out = os; + } + } + return out; + } + @Override - public void write(byte[] b, int o, int n) - throws IOException { - out.write(b, o, n); + public void write(byte[] b, int o, int n) throws IOException { + get().write(b, o, n); } @Override public void write(byte[] b) throws IOException { - out.write(b); + get().write(b); } @Override public void write(int b) throws IOException { - out.write(b); + get().write(b); } @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; try { - if (fsync) - os.getChannel().force(true); - out.close(); - os = null; + if (written) { + throw new IOException(MessageFormat + .format(JGitText.get().lockStreamClosed, ref)); + } + if (out != null) { + if (fsync) { + os.getChannel().force(true); + } + out.close(); + os = null; + } + written = true; } catch (IOException | RuntimeException | Error ioe) { unlock(); throw ioe; @@ -355,7 +373,7 @@ public class LockFile { } void requireLock() { - if (os == null) { + if (!haveLck) { unlock(); throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotHeld, ref)); } @@ -444,6 +462,8 @@ public class LockFile { try { FileUtils.rename(lck, ref, StandardCopyOption.ATOMIC_MOVE); haveLck = false; + isAppend = false; + written = false; closeToken(); return true; } catch (IOException e) { @@ -530,6 +550,8 @@ public class LockFile { closeToken(); } } + isAppend = false; + written = false; } /** {@inheritDoc} */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java index 968ade6700..b4cad35ddf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java @@ -109,7 +109,7 @@ import org.slf4j.LoggerFactory; * considered. */ public class ObjectDirectory extends FileObjectDatabase { - private final static Logger LOG = LoggerFactory + private static final Logger LOG = LoggerFactory .getLogger(ObjectDirectory.class); private static final PackList NO_PACKS = new PackList( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryInserter.java index e5a54e372c..09f2021686 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryInserter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryInserter.java @@ -109,10 +109,9 @@ class ObjectDirectoryInserter extends ObjectInserter { ObjectId id = idFor(type, data, off, len); if (!createDuplicate && db.has(id)) { return id; - } else { - File tmp = toTemp(type, data, off, len); - return insertOneObject(tmp, id, createDuplicate); } + File tmp = toTemp(type, data, off, len); + return insertOneObject(tmp, id, createDuplicate); } /** {@inheritDoc} */ @@ -141,12 +140,11 @@ class ObjectDirectoryInserter extends ObjectInserter { int actLen = IO.readFully(is, buf, 0); return insert(type, buf, 0, actLen, createDuplicate); - } else { - SHA1 md = digest(); - File tmp = toTemp(md, type, len, is); - ObjectId id = md.toObjectId(); - return insertOneObject(tmp, id, createDuplicate); } + SHA1 md = digest(); + File tmp = toTemp(md, type, len, is); + ObjectId id = md.toObjectId(); + return insertOneObject(tmp, id, createDuplicate); } private ObjectId insertOneObject( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java index 86e90c63c5..e7d917bb7a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java @@ -103,7 +103,7 @@ import org.slf4j.LoggerFactory; * objects are similar. */ public class PackFile implements Iterable<PackIndex.MutableEntry> { - private final static Logger LOG = LoggerFactory.getLogger(PackFile.class); + private static final Logger LOG = LoggerFactory.getLogger(PackFile.class); /** Sorts PackFiles to be most recently created to least recently created. */ public static final Comparator<PackFile> SORT = new Comparator<PackFile>() { @Override @@ -845,19 +845,20 @@ public class PackFile implements Iterable<PackIndex.MutableEntry> { case Constants.OBJ_TREE: case Constants.OBJ_BLOB: case Constants.OBJ_TAG: { - if (delta != null || sz < curs.getStreamFileThreshold()) + if (delta != null || sz < curs.getStreamFileThreshold()) { data = decompress(pos + p, (int) sz, curs); + } if (delta != null) { type = typeCode; break SEARCH; } - if (data != null) + if (data != null) { return new ObjectLoader.SmallObject(typeCode, data); - else - return new LargePackedWholeObject(typeCode, sz, pos, p, - this, curs.db); + } + return new LargePackedWholeObject(typeCode, sz, pos, p, + this, curs.db); } case Constants.OBJ_OFS_DELTA: { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java index e45b53ea68..9023570607 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java @@ -366,12 +366,8 @@ class PackedBatchRefUpdate extends BatchRefUpdate { List<ReceiveCommand> commands) throws IOException { // Construct a new RefList by merging the old list with the updates. // This assumes that each ref occurs at most once as a ReceiveCommand. - Collections.sort(commands, new Comparator<ReceiveCommand>() { - @Override - public int compare(ReceiveCommand a, ReceiveCommand b) { - return a.getRefName().compareTo(b.getRefName()); - } - }); + Collections.sort(commands, + Comparator.comparing(ReceiveCommand::getRefName)); int delta = 0; for (ReceiveCommand c : commands) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java index 6cb8a19f81..ae8fe0f562 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java @@ -130,7 +130,7 @@ import org.slf4j.LoggerFactory; * overall size of a Git repository on disk. */ public class RefDirectory extends RefDatabase { - private final static Logger LOG = LoggerFactory + private static final Logger LOG = LoggerFactory .getLogger(RefDirectory.class); /** Magic string denoting the start of a symbolic reference file. */ @@ -565,10 +565,9 @@ public class RefDirectory extends RefDatabase { if (obj instanceof RevTag) { return new ObjectIdRef.PeeledTag(leaf.getStorage(), leaf .getName(), leaf.getObjectId(), rw.peel(obj).copy()); - } else { - return new ObjectIdRef.PeeledNonTag(leaf.getStorage(), leaf - .getName(), leaf.getObjectId()); } + return new ObjectIdRef.PeeledNonTag(leaf.getStorage(), + leaf.getName(), leaf.getObjectId()); } } @@ -865,10 +864,9 @@ public class RefDirectory extends RefDatabase { if (peeledObjectId != null) { return new ObjectIdRef.PeeledTag(PACKED, f.getName(), f.getObjectId(), peeledObjectId); - } else { - return new ObjectIdRef.PeeledNonTag(PACKED, f.getName(), - f.getObjectId()); } + return new ObjectIdRef.PeeledNonTag(PACKED, f.getName(), + f.getObjectId()); } void log(boolean force, RefUpdate update, String msg, boolean deref) @@ -1383,7 +1381,7 @@ public class RefDirectory extends RefDatabase { LooseRef peel(ObjectIdRef newLeaf); } - private final static class LoosePeeledTag extends ObjectIdRef.PeeledTag + private static final class LoosePeeledTag extends ObjectIdRef.PeeledTag implements LooseRef { private final FileSnapshot snapShot; @@ -1404,7 +1402,7 @@ public class RefDirectory extends RefDatabase { } } - private final static class LooseNonTag extends ObjectIdRef.PeeledNonTag + private static final class LooseNonTag extends ObjectIdRef.PeeledNonTag implements LooseRef { private final FileSnapshot snapShot; @@ -1425,7 +1423,7 @@ public class RefDirectory extends RefDatabase { } } - private final static class LooseUnpeeled extends ObjectIdRef.Unpeeled + private static final class LooseUnpeeled extends ObjectIdRef.Unpeeled implements LooseRef { private FileSnapshot snapShot; @@ -1455,14 +1453,12 @@ public class RefDirectory extends RefDatabase { if (peeledObjectId != null) { return new LoosePeeledTag(snapShot, getName(), objectId, peeledObjectId); - } else { - return new LooseNonTag(snapShot, getName(), - objectId); } + return new LooseNonTag(snapShot, getName(), objectId); } } - private final static class LooseSymbolicRef extends SymbolicRef implements + private static final class LooseSymbolicRef extends SymbolicRef implements LooseRef { private final FileSnapshot snapShot; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java index 1a0d6953ab..dc4967fe4e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java @@ -90,9 +90,8 @@ class RefDirectoryUpdate extends RefUpdate { dst = database.findRef(name); setOldObjectId(dst != null ? dst.getObjectId() : null); return true; - } else { - return false; } + return false; } /** {@inheritDoc} */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java index 08a14b28d2..3cdd90408e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java @@ -140,9 +140,9 @@ public class ReflogEntryImpl implements Serializable, ReflogEntry { /** {@inheritDoc} */ @Override public CheckoutEntry parseCheckout() { - if (getComment().startsWith(CheckoutEntryImpl.CHECKOUT_MOVING_FROM)) + if (getComment().startsWith(CheckoutEntryImpl.CHECKOUT_MOVING_FROM)) { return new CheckoutEntryImpl(this); - else - return null; + } + return null; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java index 131f716cf0..98d6ea00e7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java @@ -61,6 +61,7 @@ import java.nio.channels.FileChannel; import java.text.MessageFormat; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig; import org.eclipse.jgit.lib.ObjectId; @@ -68,6 +69,7 @@ import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.ReflogEntry; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.util.FileUtils; /** @@ -239,7 +241,7 @@ public class ReflogWriter { private ReflogWriter log(String refName, byte[] rec) throws IOException { File log = refdb.logFor(refName); boolean write = forceWrite - || (isLogAllRefUpdates() && shouldAutoCreateLog(refName)) + || shouldAutoCreateLog(refName) || log.isFile(); if (!write) return this; @@ -260,15 +262,27 @@ public class ReflogWriter { return this; } - private boolean isLogAllRefUpdates() { - return refdb.getRepository().getConfig().get(CoreConfig.KEY) - .isLogAllRefUpdates(); - } - private boolean shouldAutoCreateLog(String refName) { - return refName.equals(HEAD) - || refName.startsWith(R_HEADS) - || refName.startsWith(R_REMOTES) - || refName.startsWith(R_NOTES); + Repository repo = refdb.getRepository(); + CoreConfig.LogRefUpdates value = repo.isBare() + ? CoreConfig.LogRefUpdates.FALSE + : CoreConfig.LogRefUpdates.TRUE; + value = repo.getConfig().getEnum(ConfigConstants.CONFIG_CORE_SECTION, + null, ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, value); + if (value != null) { + switch (value) { + case FALSE: + break; + case TRUE: + return refName.equals(HEAD) || refName.startsWith(R_HEADS) + || refName.startsWith(R_REMOTES) + || refName.startsWith(R_NOTES); + case ALWAYS: + return refName.equals(HEAD) || refName.startsWith(R_REFS); + default: + break; + } + } + return false; } }
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObject.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObject.java index 79f1307578..fb06623d7e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObject.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObject.java @@ -139,49 +139,48 @@ public class UnpackedObject { } return new LargeObject(type, size, path, id, wc.db); - } else { - readSome(in, hdr, 2, 18); - int c = hdr[0] & 0xff; - int type = (c >> 4) & 7; - long size = c & 15; - int shift = 4; - int p = 1; - while ((c & 0x80) != 0) { - c = hdr[p++] & 0xff; - size += ((long) (c & 0x7f)) << shift; - shift += 7; - } + } + readSome(in, hdr, 2, 18); + int c = hdr[0] & 0xff; + int type = (c >> 4) & 7; + long size = c & 15; + int shift = 4; + int p = 1; + while ((c & 0x80) != 0) { + c = hdr[p++] & 0xff; + size += ((long) (c & 0x7f)) << shift; + shift += 7; + } - switch (type) { - case Constants.OBJ_COMMIT: - case Constants.OBJ_TREE: - case Constants.OBJ_BLOB: - case Constants.OBJ_TAG: - // Acceptable types for a loose object. - break; - default: - throw new CorruptObjectException(id, - JGitText.get().corruptObjectInvalidType); - } + switch (type) { + case Constants.OBJ_COMMIT: + case Constants.OBJ_TREE: + case Constants.OBJ_BLOB: + case Constants.OBJ_TAG: + // Acceptable types for a loose object. + break; + default: + throw new CorruptObjectException(id, + JGitText.get().corruptObjectInvalidType); + } - if (path == null && Integer.MAX_VALUE < size) { - LargeObjectException.ExceedsByteArrayLimit e; - e = new LargeObjectException.ExceedsByteArrayLimit(); - e.setObjectId(id); - throw e; - } - if (size < wc.getStreamFileThreshold() || path == null) { - in.reset(); - IO.skipFully(in, p); - Inflater inf = wc.inflater(); - InputStream zIn = inflate(in, inf); - byte[] data = new byte[(int) size]; - IO.readFully(zIn, data, 0, data.length); - checkValidEndOfStream(in, inf, id, hdr); - return new ObjectLoader.SmallObject(type, data); - } - return new LargeObject(type, size, path, id, wc.db); + if (path == null && Integer.MAX_VALUE < size) { + LargeObjectException.ExceedsByteArrayLimit e; + e = new LargeObjectException.ExceedsByteArrayLimit(); + e.setObjectId(id); + throw e; } + if (size < wc.getStreamFileThreshold() || path == null) { + in.reset(); + IO.skipFully(in, p); + Inflater inf = wc.inflater(); + InputStream zIn = inflate(in, inf); + byte[] data = new byte[(int) size]; + IO.readFully(zIn, data, 0, data.length); + checkValidEndOfStream(in, inf, id, hdr); + return new ObjectLoader.SmallObject(type, data); + } + return new LargeObject(type, size, path, id, wc.db); } catch (ZipException badStream) { throw new CorruptObjectException(id, JGitText.get().corruptObjectBadStream); @@ -213,19 +212,18 @@ public class UnpackedObject { JGitText.get().corruptObjectNegativeSize); return size; - } else { - readSome(in, hdr, 2, 18); - int c = hdr[0] & 0xff; - long size = c & 15; - int shift = 4; - int p = 1; - while ((c & 0x80) != 0) { - c = hdr[p++] & 0xff; - size += ((long) (c & 0x7f)) << shift; - shift += 7; - } - return size; } + readSome(in, hdr, 2, 18); + int c = hdr[0] & 0xff; + long size = c & 15; + int shift = 4; + int p = 1; + while ((c & 0x80) != 0) { + c = hdr[p++] & 0xff; + size += ((long) (c & 0x7f)) << shift; + shift += 7; + } + return size; } catch (ZipException badStream) { throw new CorruptObjectException(id, JGitText.get().corruptObjectBadStream); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObjectCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObjectCache.java index ea0d269053..616447a651 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObjectCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/UnpackedObjectCache.java @@ -126,17 +126,19 @@ class UnpackedObjectCache { for (int n = 0; n < MAX_CHAIN;) { ObjectId obj = ids.get(i); if (obj == null) { - if (ids.compareAndSet(i, null, toAdd.copy())) + if (ids.compareAndSet(i, null, toAdd.copy())) { return true; - else - continue; + } + continue; } - if (AnyObjectId.isEqual(obj, toAdd)) + if (AnyObjectId.isEqual(obj, toAdd)) { return true; + } - if (++i == ids.length()) + if (++i == ids.length()) { i = 0; + } n++; } return false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/CachedPackUriProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/CachedPackUriProvider.java index 5cbc2baeb2..9496d78c2f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/CachedPackUriProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/CachedPackUriProvider.java @@ -76,14 +76,22 @@ public interface CachedPackUriProvider { private final String hash; private final String uri; + private final long size; + /** * Constructs an object containing information about a packfile. - * @param hash the hash of the packfile as a hexadecimal string - * @param uri the URI corresponding to the packfile + * + * @param hash + * the hash of the packfile as a hexadecimal string + * @param uri + * the URI corresponding to the packfile + * @param size + * the size of the packfile in bytes */ - public PackInfo(String hash, String uri) { + public PackInfo(String hash, String uri, long size) { this.hash = hash; this.uri = uri; + this.size = size; } /** @@ -99,5 +107,12 @@ public interface CachedPackUriProvider { public String getUri() { return uri; } + + /** + * @return the size of the packfile in bytes (-1 if unknown) + */ + public long getSize() { + return size; + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java index 292fd368cd..b4143c900c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java @@ -81,7 +81,7 @@ public class PackExt { * the file extension. * @return the PackExt for the ext */ - public synchronized static PackExt newPackExt(String ext) { + public static synchronized PackExt newPackExt(String ext) { PackExt[] dst = new PackExt[VALUES.length + 1]; for (int i = 0; i < VALUES.length; i++) { PackExt packExt = VALUES[i]; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java index 43067d364d..fcf9ca5791 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java @@ -225,7 +225,7 @@ public class PackWriter implements AutoCloseable { } @SuppressWarnings("unchecked") - BlockList<ObjectToPack> objectsLists[] = new BlockList[OBJ_TAG + 1]; + BlockList<ObjectToPack>[] objectsLists = new BlockList[OBJ_TAG + 1]; { objectsLists[OBJ_COMMIT] = new BlockList<>(); objectsLists[OBJ_TREE] = new BlockList<>(); @@ -270,7 +270,7 @@ public class PackWriter implements AutoCloseable { private List<ObjectToPack> sortedByName; - private byte packcsum[]; + private byte[] packcsum; private boolean deltaBaseAsOffset; @@ -1230,6 +1230,8 @@ public class PackWriter implements AutoCloseable { if (packInfo != null) { o.writeString(packInfo.getHash() + ' ' + packInfo.getUri() + '\n'); + stats.offloadedPackfiles += 1; + stats.offloadedPackfileSize += packInfo.getSize(); } else { unwrittenCachedPacks.add(pack); } @@ -1749,23 +1751,23 @@ public class PackWriter implements AutoCloseable { NullProgressMonitor.INSTANCE, Collections.singleton(otp)); continue; - } else { - // Object writing already started, we cannot recover. - // - CorruptObjectException coe; - coe = new CorruptObjectException(otp, ""); //$NON-NLS-1$ - coe.initCause(gone); - throw coe; } + // Object writing already started, we cannot recover. + // + CorruptObjectException coe; + coe = new CorruptObjectException(otp, ""); //$NON-NLS-1$ + coe.initCause(gone); + throw coe; } } // If we reached here, reuse wasn't possible. // - if (otp.isDeltaRepresentation()) + if (otp.isDeltaRepresentation()) { writeDeltaObjectDeflate(out, otp); - else + } else { writeWholeObjectDeflate(out, otp); + } out.endObject(); otp.setCRC((int) crc32.getValue()); } @@ -2430,7 +2432,7 @@ public class PackWriter implements AutoCloseable { } /** Possible states that a PackWriter can be in. */ - public static enum PackingPhase { + public enum PackingPhase { /** Counting objects phase. */ COUNTING, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java index b66751b94c..6ae7e45efa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockReader.java @@ -92,7 +92,6 @@ import org.eclipse.jgit.util.RawParseUtils; class BlockReader { private byte blockType; private long endPosition; - private boolean truncated; private byte[] buf; private int bufLen; @@ -112,10 +111,6 @@ class BlockReader { return blockType; } - boolean truncated() { - return truncated; - } - long endPosition() { return endPosition; } @@ -331,16 +326,8 @@ class BlockReader { // Log blocks must be inflated after the header. long deflatedSize = inflateBuf(src, pos, blockLen, fileBlockSize); endPosition = pos + 4 + deflatedSize; - } - if (bufLen < blockLen) { - if (blockType != INDEX_BLOCK_TYPE) { - throw invalidBlock(); - } - // Its OK during sequential scan for an index block to have been - // partially read and be truncated in-memory. This happens when - // the index block is larger than the file's blockSize. Caller - // will break out of its scan loop once it sees the blockType. - truncated = true; + } else if (bufLen < blockLen) { + readBlockIntoBuf(src, pos, blockLen); } else if (bufLen > blockLen) { bufLen = blockLen; } @@ -405,7 +392,7 @@ class BlockReader { } void verifyIndex() throws IOException { - if (blockType != INDEX_BLOCK_TYPE || truncated) { + if (blockType != INDEX_BLOCK_TYPE) { throw invalidBlock(); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockWriter.java index b3173e838c..1ed1801678 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/BlockWriter.java @@ -286,7 +286,7 @@ class BlockWriter { return aLen - bLen; } - static abstract class Entry { + abstract static class Entry { static int compare(Entry ea, Entry eb) { byte[] a = ea.key; byte[] b = eb.key; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/LogCursor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/LogCursor.java index 486fd28984..5c98242f10 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/LogCursor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/LogCursor.java @@ -45,6 +45,7 @@ package org.eclipse.jgit.internal.storage.reftable; import java.io.IOException; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.lib.ReflogEntry; /** @@ -78,8 +79,9 @@ public abstract class LogCursor implements AutoCloseable { /** * Get current log entry. * - * @return current log entry. + * @return current log entry. Maybe null if we are producing deletions. */ + @Nullable public abstract ReflogEntry getReflogEntry(); /** {@inheritDoc} */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java index c740bf2c37..d272d09f00 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/MergedReftable.java @@ -67,13 +67,11 @@ import org.eclipse.jgit.lib.ReflogEntry; * A {@code MergedReftable} is not thread-safe. */ public class MergedReftable extends Reftable { - private final Reftable[] tables; + private final ReftableReader[] tables; /** * Initialize a merged table reader. * <p> - * The tables in {@code tableStack} will be closed when this - * {@code MergedReftable} is closed. * * @param tableStack * stack of tables to read from. The base of the stack is at @@ -81,16 +79,44 @@ public class MergedReftable extends Reftable { * {@code tableStack.size() - 1}. The top of the stack (higher * index) shadows the base of the stack (lower index). */ - public MergedReftable(List<Reftable> tableStack) { - tables = tableStack.toArray(new Reftable[0]); + public MergedReftable(List<ReftableReader> tableStack) { + tables = tableStack.toArray(new ReftableReader[0]); // Tables must expose deletes to this instance to correctly // shadow references from lower tables. - for (Reftable t : tables) { + for (ReftableReader t : tables) { t.setIncludeDeletes(true); } } + /** + * {@inheritDoc} + */ + @Override + public long maxUpdateIndex() throws IOException { + return tables.length > 0 ? tables[tables.length - 1].maxUpdateIndex() + : 0; + } + + /** + * {@inheritDoc} + */ + @Override + public long minUpdateIndex() throws IOException { + return tables.length > 0 ? tables[0].minUpdateIndex() + : 0; + } + + /** {@inheritDoc} */ + @Override + public boolean hasObjectMap() throws IOException { + boolean has = true; + for (int i = 0; has && i < tables.length; i++) { + has = has && tables[i].hasObjectMap(); + } + return has; + } + /** {@inheritDoc} */ @Override public RefCursor allRefs() throws IOException { @@ -124,7 +150,7 @@ public class MergedReftable extends Reftable { /** {@inheritDoc} */ @Override public RefCursor byObjectId(AnyObjectId name) throws IOException { - MergedRefCursor m = new MergedRefCursor(); + MergedRefCursor m = new FilteringMergedRefCursor(name); for (int i = 0; i < tables.length; i++) { m.add(new RefQueueEntry(tables[i].byObjectId(name), i)); } @@ -152,14 +178,6 @@ public class MergedReftable extends Reftable { return m; } - /** {@inheritDoc} */ - @Override - public void close() throws IOException { - for (Reftable t : tables) { - t.close(); - } - } - int queueSize() { return Math.max(1, tables.length); } @@ -251,6 +269,42 @@ public class MergedReftable extends Reftable { } } + private class FilteringMergedRefCursor extends MergedRefCursor { + final AnyObjectId filterId; + Ref filteredRef; + + FilteringMergedRefCursor(AnyObjectId id) { + filterId = id; + filteredRef = null; + } + + @Override + public Ref getRef() { + return filteredRef; + } + + @Override + public boolean next() throws IOException { + for (;;) { + boolean ok = super.next(); + if (!ok) { + return false; + } + + String name = super.getRef().getName(); + + try (RefCursor c = seekRef(name)) { + if (c.next()) { + if (filterId.equals(c.getRef().getObjectId())) { + filteredRef = c.getRef(); + return true; + } + } + } + } + } + } + private static class RefQueueEntry { static int compare(RefQueueEntry a, RefQueueEntry b) { int cmp = a.name().compareTo(b.name()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java index cb02628e8d..a7e8252b48 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/Reftable.java @@ -58,7 +58,7 @@ import org.eclipse.jgit.lib.SymbolicRef; /** * Abstract table of references. */ -public abstract class Reftable implements AutoCloseable { +public abstract class Reftable { /** * References to convert into a reftable * @@ -72,9 +72,9 @@ public abstract class Reftable implements AutoCloseable { cfg.setIndexObjects(false); cfg.setAlignBlocks(false); ByteArrayOutputStream buf = new ByteArrayOutputStream(); - new ReftableWriter() + new ReftableWriter(buf) .setConfig(cfg) - .begin(buf) + .begin() .sortAndWriteRefs(refs) .finish(); return new ReftableReader(BlockSource.from(buf.toByteArray())); @@ -99,6 +99,28 @@ public abstract class Reftable implements AutoCloseable { } /** + * Get the maximum update index for ref entries that appear in this + * reftable. + * + * @return the maximum update index for ref entries that appear in this + * reftable. + * @throws java.io.IOException + * file cannot be read. + */ + public abstract long maxUpdateIndex() throws IOException; + + /** + * Get the minimum update index for ref entries that appear in this + * reftable. + * + * @return the minimum update index for ref entries that appear in this + * reftable. + * @throws java.io.IOException + * file cannot be read. + */ + public abstract long minUpdateIndex() throws IOException; + + /** * Seek to the first reference, to iterate in order. * * @return cursor to iterate. @@ -149,6 +171,12 @@ public abstract class Reftable implements AutoCloseable { public abstract RefCursor byObjectId(AnyObjectId id) throws IOException; /** + * @return whether this reftable can do a fast SHA1 => ref lookup. + * @throws IOException on I/O problems. + */ + public abstract boolean hasObjectMap() throws IOException; + + /** * Seek reader to read log records. * * @return cursor to iterate; empty cursor if no logs are present. @@ -282,8 +310,4 @@ public abstract class Reftable implements AutoCloseable { } return new SymbolicRef(ref.getName(), dst, ref.getUpdateIndex()); } - - /** {@inheritDoc} */ - @Override - public abstract void close() throws IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReftableBatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableBatchRefUpdate.java index 07fd00f149..592120d89b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReftableBatchRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableBatchRefUpdate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017, Google Inc. + * Copyright (C) 2019, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -41,40 +41,11 @@ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.eclipse.jgit.internal.storage.dfs; - -import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE; -import static org.eclipse.jgit.lib.Ref.Storage.NEW; -import static org.eclipse.jgit.lib.Ref.Storage.PACKED; -import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE; -import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; -import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK; -import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT; -import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD; -import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.locks.ReentrantLock; +package org.eclipse.jgit.internal.storage.reftable; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; -import org.eclipse.jgit.internal.storage.io.BlockSource; -import org.eclipse.jgit.internal.storage.pack.PackExt; -import org.eclipse.jgit.internal.storage.reftable.Reftable; -import org.eclipse.jgit.internal.storage.reftable.ReftableCompactor; -import org.eclipse.jgit.internal.storage.reftable.ReftableConfig; -import org.eclipse.jgit.internal.storage.reftable.ReftableReader; -import org.eclipse.jgit.internal.storage.reftable.ReftableWriter; +import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.ObjectId; @@ -82,43 +53,65 @@ import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.ReflogEntry; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.SymbolicRef; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; -/** - * {@link org.eclipse.jgit.lib.BatchRefUpdate} for - * {@link org.eclipse.jgit.internal.storage.dfs.DfsReftableDatabase}. - */ -public class ReftableBatchRefUpdate extends BatchRefUpdate { - private static final int AVG_BYTES = 36; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.locks.Lock; +import java.util.stream.Collectors; + +import static org.eclipse.jgit.lib.Ref.Storage.NEW; +import static org.eclipse.jgit.lib.Ref.Storage.PACKED; - private final DfsReftableDatabase refdb; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD; - private final DfsObjDatabase odb; +/** + * {@link org.eclipse.jgit.lib.BatchRefUpdate} for Reftable based RefDatabase. + */ +public abstract class ReftableBatchRefUpdate extends BatchRefUpdate { + private final Lock lock; - private final ReentrantLock lock; + private final ReftableDatabase refDb; - private final ReftableConfig reftableConfig; + private final Repository repository; /** - * Initialize batch update. + * Initialize. * * @param refdb - * database the update will modify. - * @param odb - * object database to store the reftable. + * The RefDatabase + * @param reftableDb + * The ReftableDatabase + * @param lock + * A lock protecting the refdatabase's state + * @param repository + * The repository on which this update will run */ - protected ReftableBatchRefUpdate(DfsReftableDatabase refdb, - DfsObjDatabase odb) { + protected ReftableBatchRefUpdate(RefDatabase refdb, ReftableDatabase reftableDb, Lock lock, + Repository repository) { super(refdb); - this.refdb = refdb; - this.odb = odb; - lock = refdb.getLock(); - reftableConfig = refdb.getReftableConfig(); + this.refDb = reftableDb; + this.lock = lock; + this.repository = repository; } /** {@inheritDoc} */ @@ -135,25 +128,36 @@ public class ReftableBatchRefUpdate extends BatchRefUpdate { if (!checkObjectExistence(rw, pending)) { return; } + // if we are here, checkObjectExistence might have flagged some problems + // but the transaction is not atomic, so we should proceed with the other + // pending commands. + pending = getPending(); if (!checkNonFastForwards(rw, pending)) { return; } + pending = getPending(); lock.lock(); try { - Reftable table = refdb.reader(); - if (!checkExpected(table, pending)) { + if (!checkExpected(pending)) { return; } + pending = getPending(); if (!checkConflicting(pending)) { return; } + pending = getPending(); if (!blockUntilTimestamps(MAX_WAIT)) { return; } - applyUpdates(rw, pending); + + List<Ref> newRefs = toNewRefs(rw, pending); + applyUpdates(newRefs, pending); for (ReceiveCommand cmd : pending) { - cmd.setResult(OK); + if (cmd.getResult() == NOT_ATTEMPTED) { + // XXX this is a bug in DFS ? + cmd.setResult(OK); + } } } finally { lock.unlock(); @@ -164,6 +168,19 @@ public class ReftableBatchRefUpdate extends BatchRefUpdate { } } + /** + * Implements the storage-specific part of the update. + * + * @param newRefs + * the new refs to create + * @param pending + * the pending receive commands to be executed + * @throws IOException + * if any of the writes fail. + */ + protected abstract void applyUpdates(List<Ref> newRefs, + List<ReceiveCommand> pending) throws IOException; + private List<ReceiveCommand> getPending() { return ReceiveCommand.filter(getCommands(), NOT_ATTEMPTED); } @@ -181,8 +198,10 @@ public class ReftableBatchRefUpdate extends BatchRefUpdate { // used for a missing object. Eagerly handle this case so we // can set the right result. cmd.setResult(REJECTED_MISSING_OBJECT); - ReceiveCommand.abort(pending); - return false; + if (isAtomic()) { + ReceiveCommand.abort(pending); + return false; + } } } return true; @@ -197,8 +216,10 @@ public class ReftableBatchRefUpdate extends BatchRefUpdate { cmd.updateType(rw); if (cmd.getType() == UPDATE_NONFASTFORWARD) { cmd.setResult(REJECTED_NONFASTFORWARD); - ReceiveCommand.abort(pending); - return false; + if (isAtomic()) { + ReceiveCommand.abort(pending); + return false; + } } } return true; @@ -206,40 +227,55 @@ public class ReftableBatchRefUpdate extends BatchRefUpdate { private boolean checkConflicting(List<ReceiveCommand> pending) throws IOException { - Set<String> names = new HashSet<>(); - for (ReceiveCommand cmd : pending) { - names.add(cmd.getRefName()); - } + TreeSet<String> added = new TreeSet<>(); + Set<String> deleted = + pending.stream() + .filter(cmd -> cmd.getType() == DELETE) + .map(c -> c.getRefName()) + .collect(Collectors.toSet()); boolean ok = true; for (ReceiveCommand cmd : pending) { + if (cmd.getType() == DELETE) { + continue; + } + String name = cmd.getRefName(); - if (refdb.isNameConflicting(name)) { - cmd.setResult(LOCK_FAILURE); - ok = false; - } else { - int s = name.lastIndexOf('/'); - while (0 < s) { - if (names.contains(name.substring(0, s))) { - cmd.setResult(LOCK_FAILURE); - ok = false; - break; - } - s = name.lastIndexOf('/', s - 1); + if (refDb.isNameConflicting(name, added, deleted)) { + if (isAtomic()) { + cmd.setResult( + ReceiveCommand.Result.REJECTED_OTHER_REASON, JGitText.get().transactionAborted); + } else { + cmd.setResult(LOCK_FAILURE); } + + ok = false; } + added.add(name); } - if (!ok && isAtomic()) { - ReceiveCommand.abort(pending); - return false; + + if (isAtomic()) { + if (!ok) { + pending.stream() + .filter(cmd -> cmd.getResult() == NOT_ATTEMPTED) + .forEach(cmd -> cmd.setResult(LOCK_FAILURE)); + } + return ok; } - return ok; + + for (ReceiveCommand cmd : pending) { + if (cmd.getResult() == NOT_ATTEMPTED) { + return true; + } + } + + return false; } - private boolean checkExpected(Reftable table, List<ReceiveCommand> pending) + private boolean checkExpected(List<ReceiveCommand> pending) throws IOException { for (ReceiveCommand cmd : pending) { - if (!matchOld(cmd, table.exactRef(cmd.getRefName()))) { + if (!matchOld(cmd, refDb.exactRef(cmd.getRefName()))) { cmd.setResult(LOCK_FAILURE); if (isAtomic()) { ReceiveCommand.abort(pending); @@ -253,7 +289,7 @@ public class ReftableBatchRefUpdate extends BatchRefUpdate { private static boolean matchOld(ReceiveCommand cmd, @Nullable Ref ref) { if (ref == null) { return AnyObjectId.isEqual(ObjectId.zeroId(), cmd.getOldId()) - && cmd.getOldSymref() == null; + && cmd.getOldSymref() == null; } else if (ref.isSymbolic()) { return ref.getTarget().getName().equals(cmd.getOldSymref()); } @@ -264,47 +300,26 @@ public class ReftableBatchRefUpdate extends BatchRefUpdate { return cmd.getOldId().equals(id); } - private void applyUpdates(RevWalk rw, List<ReceiveCommand> pending) - throws IOException { - List<Ref> newRefs = toNewRefs(rw, pending); - long updateIndex = nextUpdateIndex(); - Set<DfsPackDescription> prune = Collections.emptySet(); - DfsPackDescription pack = odb.newPack(PackSource.INSERT); - try (DfsOutputStream out = odb.writeFile(pack, REFTABLE)) { - ReftableConfig cfg = DfsPackCompactor - .configureReftable(reftableConfig, out); - - ReftableWriter.Stats stats; - if (refdb.compactDuringCommit() - && newRefs.size() * AVG_BYTES <= cfg.getRefBlockSize() - && canCompactTopOfStack(cfg)) { - ByteArrayOutputStream tmp = new ByteArrayOutputStream(); - write(tmp, cfg, updateIndex, newRefs, pending); - stats = compactTopOfStack(out, cfg, tmp.toByteArray()); - prune = toPruneTopOfStack(); - } else { - stats = write(out, cfg, updateIndex, newRefs, pending); - } - pack.addFileExt(REFTABLE); - pack.setReftableStats(stats); - } - - odb.commitPack(Collections.singleton(pack), prune); - odb.addReftable(pack, prune); - refdb.clearCache(); - } - - private ReftableWriter.Stats write(OutputStream os, ReftableConfig cfg, - long updateIndex, List<Ref> newRefs, List<ReceiveCommand> pending) - throws IOException { - ReftableWriter writer = new ReftableWriter(cfg) - .setMinUpdateIndex(updateIndex).setMaxUpdateIndex(updateIndex) - .begin(os).sortAndWriteRefs(newRefs); + /** + * Writes the refs to the writer, and calls finish. + * + * @param writer + * the writer on which we should write. + * @param newRefs + * the ref data to write.. + * @param pending + * the log data to write. + * @throws IOException + * in case of problems. + */ + protected void write(ReftableWriter writer, List<Ref> newRefs, + List<ReceiveCommand> pending) throws IOException { + long updateIndex = refDb.nextUpdateIndex(); + writer.setMinUpdateIndex(updateIndex).setMaxUpdateIndex(updateIndex) + .begin().sortAndWriteRefs(newRefs); if (!isRefLogDisabled()) { writeLog(writer, updateIndex, pending); } - writer.finish(); - return writer.getStats(); } private void writeLog(ReftableWriter writer, long updateIndex, @@ -319,7 +334,7 @@ public class ReftableBatchRefUpdate extends BatchRefUpdate { PersonIdent ident = getRefLogIdent(); if (ident == null) { - ident = new PersonIdent(refdb.getRepository()); + ident = new PersonIdent(repository); } for (String name : byName) { ReceiveCommand cmd = cmds.get(name); @@ -361,20 +376,25 @@ public class ReftableBatchRefUpdate extends BatchRefUpdate { } } + // Extracts and peels the refs out of the ReceiveCommands private static List<Ref> toNewRefs(RevWalk rw, List<ReceiveCommand> pending) - throws IOException { + throws IOException { List<Ref> refs = new ArrayList<>(pending.size()); for (ReceiveCommand cmd : pending) { + if (cmd.getResult() != NOT_ATTEMPTED) { + continue; + } + String name = cmd.getRefName(); ObjectId newId = cmd.getNewId(); String newSymref = cmd.getNewSymref(); if (AnyObjectId.isEqual(ObjectId.zeroId(), newId) - && newSymref == null) { + && newSymref == null) { refs.add(new ObjectIdRef.Unpeeled(NEW, name, null)); continue; } else if (newSymref != null) { refs.add(new SymbolicRef(name, - new ObjectIdRef.Unpeeled(NEW, newSymref, null))); + new ObjectIdRef.Unpeeled(NEW, newSymref, null))); continue; } @@ -385,76 +405,11 @@ public class ReftableBatchRefUpdate extends BatchRefUpdate { } if (peel != null) { refs.add(new ObjectIdRef.PeeledTag(PACKED, name, newId, - peel.copy())); + peel.copy())); } else { refs.add(new ObjectIdRef.PeeledNonTag(PACKED, name, newId)); } } return refs; } - - private long nextUpdateIndex() throws IOException { - long updateIndex = 0; - for (Reftable r : refdb.stack().readers()) { - if (r instanceof ReftableReader) { - updateIndex = Math.max(updateIndex, - ((ReftableReader) r).maxUpdateIndex()); - } - } - return updateIndex + 1; - } - - private boolean canCompactTopOfStack(ReftableConfig cfg) - throws IOException { - ReftableStack stack = refdb.stack(); - List<Reftable> readers = stack.readers(); - if (readers.isEmpty()) { - return false; - } - - int lastIdx = readers.size() - 1; - DfsReftable last = stack.files().get(lastIdx); - DfsPackDescription desc = last.getPackDescription(); - if (desc.getPackSource() != PackSource.INSERT - || !packOnlyContainsReftable(desc)) { - return false; - } - - Reftable table = readers.get(lastIdx); - int bs = cfg.getRefBlockSize(); - return table instanceof ReftableReader - && ((ReftableReader) table).size() <= 3 * bs; - } - - private ReftableWriter.Stats compactTopOfStack(OutputStream out, - ReftableConfig cfg, byte[] newTable) throws IOException { - List<Reftable> stack = refdb.stack().readers(); - Reftable last = stack.get(stack.size() - 1); - - List<Reftable> tables = new ArrayList<>(2); - tables.add(last); - tables.add(new ReftableReader(BlockSource.from(newTable))); - - ReftableCompactor compactor = new ReftableCompactor(); - compactor.setConfig(cfg); - compactor.setIncludeDeletes(true); - compactor.addAll(tables); - compactor.compact(out); - return compactor.getStats(); - } - - private Set<DfsPackDescription> toPruneTopOfStack() throws IOException { - List<DfsReftable> stack = refdb.stack().files(); - DfsReftable last = stack.get(stack.size() - 1); - return Collections.singleton(last.getPackDescription()); - } - - private boolean packOnlyContainsReftable(DfsPackDescription desc) { - for (PackExt ext : PackExt.values()) { - if (ext != REFTABLE && desc.hasFileExt(ext)) { - return false; - } - } - return true; - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableCompactor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableCompactor.java index c4e8f69fa4..a586d42de6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableCompactor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableCompactor.java @@ -61,45 +61,42 @@ import org.eclipse.jgit.lib.ReflogEntry; * to shadow any lower reftable that may have the reference present. * <p> * By default all log entries within the range defined by - * {@link #setMinUpdateIndex(long)} and {@link #setMaxUpdateIndex(long)} are + * {@link #setReflogExpireMinUpdateIndex(long)} and {@link #setReflogExpireMaxUpdateIndex(long)} are * copied, even if no references in the output file match the log records. * Callers may truncate the log to a more recent time horizon with - * {@link #setOldestReflogTimeMillis(long)}, or disable the log altogether with + * {@link #setReflogExpireOldestReflogTimeMillis(long)}, or disable the log altogether with * {@code setOldestReflogTimeMillis(Long.MAX_VALUE)}. */ public class ReftableCompactor { - private final ReftableWriter writer = new ReftableWriter(); - private final ArrayDeque<Reftable> tables = new ArrayDeque<>(); + private final ReftableWriter writer; + private final ArrayDeque<ReftableReader> tables = new ArrayDeque<>(); - private long compactBytesLimit; - private long bytesToCompact; private boolean includeDeletes; - private long minUpdateIndex = -1; - private long maxUpdateIndex; - private long oldestReflogTimeMillis; + private long reflogExpireMinUpdateIndex = 0; + private long reflogExpireMaxUpdateIndex = Long.MAX_VALUE; + private long reflogExpireOldestReflogTimeMillis; private Stats stats; /** - * Set configuration for the reftable. + * Creates a new compactor. * - * @param cfg - * configuration for the reftable. - * @return {@code this} + * @param out + * stream to write the compacted tables to. Caller is responsible + * for closing {@code out}. */ - public ReftableCompactor setConfig(ReftableConfig cfg) { - writer.setConfig(cfg); - return this; + public ReftableCompactor(OutputStream out) { + writer = new ReftableWriter(out); } /** - * Set limit on number of bytes from source tables to compact. + * Set configuration for the reftable. * - * @param bytes - * limit on number of bytes from source tables to compact. + * @param cfg + * configuration for the reftable. * @return {@code this} */ - public ReftableCompactor setCompactBytesLimit(long bytes) { - compactBytesLimit = bytes; + public ReftableCompactor setConfig(ReftableConfig cfg) { + writer.setConfig(cfg); return this; } @@ -128,8 +125,8 @@ public class ReftableCompactor { * in a stack. * @return {@code this} */ - public ReftableCompactor setMinUpdateIndex(long min) { - minUpdateIndex = min; + public ReftableCompactor setReflogExpireMinUpdateIndex(long min) { + reflogExpireMinUpdateIndex = min; return this; } @@ -144,8 +141,8 @@ public class ReftableCompactor { * used in a stack. * @return {@code this} */ - public ReftableCompactor setMaxUpdateIndex(long max) { - maxUpdateIndex = max; + public ReftableCompactor setReflogExpireMaxUpdateIndex(long max) { + reflogExpireMaxUpdateIndex = max; return this; } @@ -159,16 +156,13 @@ public class ReftableCompactor { * Specified in Java standard milliseconds since the epoch. * @return {@code this} */ - public ReftableCompactor setOldestReflogTimeMillis(long timeMillis) { - oldestReflogTimeMillis = timeMillis; + public ReftableCompactor setReflogExpireOldestReflogTimeMillis(long timeMillis) { + reflogExpireOldestReflogTimeMillis = timeMillis; return this; } /** * Add all of the tables, in the specified order. - * <p> - * Unconditionally adds all tables, ignoring the - * {@link #setCompactBytesLimit(long)}. * * @param readers * tables to compact. Tables should be ordered oldest first/most @@ -177,67 +171,26 @@ public class ReftableCompactor { * @throws java.io.IOException * update indexes of a reader cannot be accessed. */ - public void addAll(List<? extends Reftable> readers) throws IOException { - tables.addAll(readers); - for (Reftable r : readers) { - if (r instanceof ReftableReader) { - adjustUpdateIndexes((ReftableReader) r); - } + public void addAll(List<ReftableReader> readers) throws IOException { + for (ReftableReader r : readers) { + tables.add(r); } } /** - * Try to add this reader at the bottom of the stack. - * <p> - * A reader may be rejected by returning {@code false} if the compactor is - * already rewriting its {@link #setCompactBytesLimit(long)}. When this - * happens the caller should stop trying to add tables, and execute the - * compaction. - * - * @param reader - * the reader to insert at the bottom of the stack. Caller is - * responsible for closing the reader. - * @return {@code true} if the compactor accepted this table; {@code false} - * if the compactor has reached its limit. - * @throws java.io.IOException - * if size of {@code reader}, or its update indexes cannot be read. - */ - public boolean tryAddFirst(ReftableReader reader) throws IOException { - long sz = reader.size(); - if (compactBytesLimit > 0 && bytesToCompact + sz > compactBytesLimit) { - return false; - } - bytesToCompact += sz; - adjustUpdateIndexes(reader); - tables.addFirst(reader); - return true; - } - - private void adjustUpdateIndexes(ReftableReader reader) throws IOException { - if (minUpdateIndex == -1) { - minUpdateIndex = reader.minUpdateIndex(); - } else { - minUpdateIndex = Math.min(minUpdateIndex, reader.minUpdateIndex()); - } - maxUpdateIndex = Math.max(maxUpdateIndex, reader.maxUpdateIndex()); - } - - /** * Write a compaction to {@code out}. * - * @param out - * stream to write the compacted tables to. Caller is responsible - * for closing {@code out}. * @throws java.io.IOException * if tables cannot be read, or cannot be written. */ - public void compact(OutputStream out) throws IOException { + public void compact() throws IOException { MergedReftable mr = new MergedReftable(new ArrayList<>(tables)); mr.setIncludeDeletes(includeDeletes); - writer.setMinUpdateIndex(Math.max(minUpdateIndex, 0)); - writer.setMaxUpdateIndex(maxUpdateIndex); - writer.begin(out); + writer.setMaxUpdateIndex(mr.maxUpdateIndex()); + writer.setMinUpdateIndex(mr.minUpdateIndex()); + + writer.begin(); mergeRefs(mr); mergeLogs(mr); writer.finish(); @@ -262,16 +215,14 @@ public class ReftableCompactor { } private void mergeLogs(MergedReftable mr) throws IOException { - if (oldestReflogTimeMillis == Long.MAX_VALUE) { + if (reflogExpireOldestReflogTimeMillis == Long.MAX_VALUE) { return; } try (LogCursor lc = mr.allLogs()) { while (lc.next()) { long updateIndex = lc.getUpdateIndex(); - if (updateIndex < minUpdateIndex - || updateIndex > maxUpdateIndex) { - // Cannot merge log records outside the header's range. + if (updateIndex > reflogExpireMaxUpdateIndex || updateIndex < reflogExpireMinUpdateIndex) { continue; } @@ -285,7 +236,7 @@ public class ReftableCompactor { } PersonIdent who = log.getWho(); - if (who.getWhen().getTime() >= oldestReflogTimeMillis) { + if (who.getWhen().getTime() >= reflogExpireOldestReflogTimeMillis) { writer.writeLog( refName, updateIndex, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java new file mode 100644 index 0000000000..696347e8fe --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableDatabase.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2017, Google LLC + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.ReflogReader; +import org.eclipse.jgit.transport.ReceiveCommand; + +/** + * Operations on {@link MergedReftable} that is common to various reftable-using + * subclasses of {@link RefDatabase}. See + * {@link org.eclipse.jgit.internal.storage.dfs.DfsReftableDatabase} for an + * example. + */ +public abstract class ReftableDatabase { + // Protects mergedTables. + private final ReentrantLock lock = new ReentrantLock(true); + + private Reftable mergedTables; + + /** + * ReftableDatabase lazily initializes its merged reftable on the first read after + * construction or clearCache() call. This function should always instantiate a new + * MergedReftable based on the list of reftables specified by the underlying storage. + * + * @return the ReftableStack for this instance + * @throws IOException + * on I/O problems. + */ + protected abstract MergedReftable openMergedReftable() throws IOException; + + /** + * @return the next available logical timestamp for an additional reftable + * in the stack. + * @throws java.io.IOException + * on I/O problems. + */ + public long nextUpdateIndex() throws IOException { + lock.lock(); + try { + return reader().maxUpdateIndex() + 1; + } finally { + lock.unlock(); + } + } + + /** + * @return a ReflogReader for the given ref + * @param refname + * the name of the ref. + * @throws IOException + * on I/O problems + */ + public ReflogReader getReflogReader(String refname) throws IOException { + lock.lock(); + try { + return new ReftableReflogReader(lock, reader(), refname); + } finally { + lock.unlock(); + } + } + + /** + * @return a ReceiveCommand for the change from oldRef to newRef + * @param oldRef + * a ref + * @param newRef + * a ref + */ + public static ReceiveCommand toCommand(Ref oldRef, Ref newRef) { + ObjectId oldId = toId(oldRef); + ObjectId newId = toId(newRef); + String name = oldRef != null ? oldRef.getName() : newRef.getName(); + + if (oldRef != null && oldRef.isSymbolic()) { + if (newRef != null) { + if (newRef.isSymbolic()) { + return ReceiveCommand.link(oldRef.getTarget().getName(), + newRef.getTarget().getName(), name); + } + // This should pass in oldId for compat with + // RefDirectoryUpdate + return ReceiveCommand.unlink(oldRef.getTarget().getName(), + newId, name); + } + return ReceiveCommand.unlink(oldRef.getTarget().getName(), + ObjectId.zeroId(), name); + } + + if (newRef != null && newRef.isSymbolic()) { + if (oldRef != null) { + if (oldRef.isSymbolic()) { + return ReceiveCommand.link(oldRef.getTarget().getName(), + newRef.getTarget().getName(), name); + } + return ReceiveCommand.link(oldId, + newRef.getTarget().getName(), name); + } + return ReceiveCommand.link(ObjectId.zeroId(), + newRef.getTarget().getName(), name); + } + + return new ReceiveCommand(oldId, newId, name); + } + + private static ObjectId toId(Ref ref) { + if (ref != null) { + ObjectId id = ref.getObjectId(); + if (id != null) { + return id; + } + } + return ObjectId.zeroId(); + } + + /** + * @return the lock protecting underlying ReftableReaders against concurrent + * reads. + */ + public ReentrantLock getLock() { + return lock; + } + + /** + * @return the merged reftable that is implemented by the stack of + * reftables. Return value must be accessed under lock. + * @throws IOException + * on I/O problems + */ + private Reftable reader() throws IOException { + if (!lock.isLocked()) { + throw new IllegalStateException( + "must hold lock to access merged table"); //$NON-NLS-1$ + } + if (mergedTables == null) { + mergedTables = openMergedReftable(); + } + return mergedTables; + } + + /** + * @return whether the given refName would be illegal in a repository that + * uses loose refs. + * @param refName + * the name to check + * @param added + * a sorted set of refs we pretend have been added to the + * database. + * @param deleted + * a set of refs we pretend have been removed from the database. + * @throws IOException + * on I/O problems + */ + public boolean isNameConflicting(String refName, TreeSet<String> added, + Set<String> deleted) throws IOException { + lock.lock(); + try { + Reftable table = reader(); + + // Cannot be nested within an existing reference. + int lastSlash = refName.lastIndexOf('/'); + while (0 < lastSlash) { + String prefix = refName.substring(0, lastSlash); + if (!deleted.contains(prefix) + && (table.hasRef(prefix) || added.contains(prefix))) { + return true; + } + lastSlash = refName.lastIndexOf('/', lastSlash - 1); + } + + // Cannot be the container of an existing reference. + String prefix = refName + '/'; + RefCursor c = table.seekRefsWithPrefix(prefix); + while (c.next()) { + if (!deleted.contains(c.getRef().getName())) { + return true; + } + } + + String it = added.ceiling(refName + '/'); + if (it != null && it.startsWith(prefix)) { + return true; + } + return false; + } finally { + lock.unlock(); + } + } + + /** + * Read a single reference. + * <p> + * This method expects an unshortened reference name and does not search + * using the standard search path. + * + * @param name + * the unabbreviated name of the reference. + * @return the reference (if it exists); else {@code null}. + * @throws java.io.IOException + * the reference space cannot be accessed. + */ + @Nullable + public Ref exactRef(String name) throws IOException { + lock.lock(); + try { + Reftable table = reader(); + Ref ref = table.exactRef(name); + if (ref != null && ref.isSymbolic()) { + return table.resolve(ref); + } + return ref; + } finally { + lock.unlock(); + } + } + + /** + * Returns refs whose names start with a given prefix. + * + * @param prefix + * string that names of refs should start with; may be empty (to + * return all refs). + * @return immutable list of refs whose names start with {@code prefix}. + * @throws java.io.IOException + * the reference space cannot be accessed. + */ + public List<Ref> getRefsByPrefix(String prefix) throws IOException { + List<Ref> all = new ArrayList<>(); + lock.lock(); + try { + Reftable table = reader(); + try (RefCursor rc = RefDatabase.ALL.equals(prefix) ? table.allRefs() + : table.seekRefsWithPrefix(prefix)) { + while (rc.next()) { + Ref ref = table.resolve(rc.getRef()); + if (ref != null && ref.getObjectId() != null) { + all.add(ref); + } + } + } + } finally { + lock.unlock(); + } + + return Collections.unmodifiableList(all); + } + + /** + * @return whether there is a fast SHA1 to ref map. + * @throws IOException in case of I/O problems. + */ + public boolean hasFastTipsWithSha1() throws IOException { + lock.lock(); + try { + return reader().hasObjectMap(); + } finally { + lock.unlock(); + } + } + + /** + * Returns all refs that resolve directly to the given {@link ObjectId}. + * Includes peeled {@linkObjectId}s. + * + * @param id + * {@link ObjectId} to resolve + * @return a {@link Set} of {@link Ref}s whose tips point to the provided + * id. + * @throws java.io.IOException + * on I/O errors. + */ + public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException { + lock.lock(); + try { + RefCursor cursor = reader().byObjectId(id); + Set<Ref> refs = new HashSet<>(); + while (cursor.next()) { + refs.add(cursor.getRef()); + } + return refs; + } finally { + lock.unlock(); + } + } + + /** + * Drops all data that might be cached in memory. + */ + public void clearCache() { + lock.lock(); + try { + mergedTables = null; + } finally { + lock.unlock(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java index 4f0ff2d1d1..3b912ea1c2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReader.java @@ -78,7 +78,7 @@ import org.eclipse.jgit.util.NB; * {@code ReftableReader} is not thread-safe. Concurrent readers need their own * instance to read from the same file. */ -public class ReftableReader extends Reftable { +public class ReftableReader extends Reftable implements AutoCloseable { private final BlockSource src; private int blockSize = -1; @@ -128,16 +128,20 @@ public class ReftableReader extends Reftable { return blockSize; } + @Override + public boolean hasObjectMap() throws IOException { + if (objIndexPosition == -1) { + readFileFooter(); + } + + // We have the map, we have no refs, or the table is small. + return (objPosition > 0 || refEnd == 24 || refIndexPosition == 0); + } + /** - * Get the minimum update index for log entries that appear in this - * reftable. - * - * @return the minimum update index for log entries that appear in this - * reftable. This should be 1 higher than the prior reftable's - * {@code maxUpdateIndex} if this table is used in a stack. - * @throws java.io.IOException - * file cannot be read. + * {@inheritDoc} */ + @Override public long minUpdateIndex() throws IOException { if (blockSize == -1) { readFileHeader(); @@ -146,15 +150,9 @@ public class ReftableReader extends Reftable { } /** - * Get the maximum update index for log entries that appear in this - * reftable. - * - * @return the maximum update index for log entries that appear in this - * reftable. This should be 1 higher than the prior reftable's - * {@code maxUpdateIndex} if this table is used in a stack. - * @throws java.io.IOException - * file cannot be read. + * {@inheritDoc} */ + @Override public long maxUpdateIndex() throws IOException { if (blockSize == -1) { readFileHeader(); @@ -169,11 +167,13 @@ public class ReftableReader extends Reftable { readFileHeader(); } - long end = refEnd > 0 ? refEnd : (src.size() - FILE_FOOTER_LEN); - src.adviseSequentialRead(0, end); + if (refEnd == 0) { + readFileFooter(); + } + src.adviseSequentialRead(0, refEnd); - RefCursorImpl i = new RefCursorImpl(end, null, false); - i.block = readBlock(0, end); + RefCursorImpl i = new RefCursorImpl(refEnd, null, false); + i.block = readBlock(0, refEnd); return i; } @@ -468,7 +468,7 @@ public class ReftableReader extends Reftable { BlockReader b = new BlockReader(); b.readBlock(src, pos, sz); - if (b.type() == INDEX_BLOCK_TYPE && !b.truncated()) { + if (b.type() == INDEX_BLOCK_TYPE) { if (indexCache == null) { indexCache = new LongMap<>(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReflogReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReflogReader.java new file mode 100644 index 0000000000..c75d3cfa55 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableReflogReader.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2019, Google LLC + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftable; + +import org.eclipse.jgit.lib.ReflogEntry; +import org.eclipse.jgit.lib.ReflogReader; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.Lock; + +/** + * Implement the ReflogReader interface for a reflog stored in reftable. + */ +public class ReftableReflogReader implements ReflogReader { + private final Lock lock; + + private final Reftable reftable; + + private final String refname; + + ReftableReflogReader(Lock lock, Reftable merged, String refname) { + this.lock = lock; + this.reftable = merged; + this.refname = refname; + } + + /** {@inheritDoc} */ + @Override + public ReflogEntry getLastEntry() throws IOException { + lock.lock(); + try { + LogCursor cursor = reftable.seekLog(refname); + return cursor.next() ? cursor.getReflogEntry() : null; + } finally { + lock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public List<ReflogEntry> getReverseEntries() throws IOException { + return getReverseEntries(Integer.MAX_VALUE); + } + + /** {@inheritDoc} */ + @Override + public ReflogEntry getReverseEntry(int number) throws IOException { + lock.lock(); + try { + LogCursor cursor = reftable.seekLog(refname); + while (true) { + if (!cursor.next() || number < 0) { + return null; + } + if (number == 0) { + return cursor.getReflogEntry(); + } + number--; + } + } finally { + lock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public List<ReflogEntry> getReverseEntries(int max) throws IOException { + lock.lock(); + try { + LogCursor cursor = reftable.seekLog(refname); + + List<ReflogEntry> result = new ArrayList<>(); + while (cursor.next() && result.size() < max) { + result.add(cursor.getReflogEntry()); + } + + return result; + } finally { + lock.unlock(); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableWriter.java index 6459c2763d..96f8cb163a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftable/ReftableWriter.java @@ -108,6 +108,7 @@ public class ReftableWriter { private long minUpdateIndex; private long maxUpdateIndex; + private OutputStream outputStream; private ReftableOutputStream out; private ObjectIdSubclassMap<RefList> obj2ref; @@ -122,21 +123,27 @@ public class ReftableWriter { /** * Initialize a writer with a default configuration. + * + * @param os + * output stream. */ - public ReftableWriter() { - this(new ReftableConfig()); + public ReftableWriter(OutputStream os) { + this(new ReftableConfig(), os); lastRef = null; lastLog = null; } /** - * Initialize a writer with a specific configuration. + * Initialize a writer with a configuration. * * @param cfg - * configuration for the writer. + * configuration for the writer + * @param os + * output stream. */ - public ReftableWriter(ReftableConfig cfg) { + public ReftableWriter(ReftableConfig cfg, OutputStream os) { config = cfg; + outputStream = os; } /** @@ -183,16 +190,16 @@ public class ReftableWriter { } /** - * Begin writing the reftable. + * Begin writing the reftable. Should be called only once. Call this + * if a stream was passed to the constructor. * - * @param os - * stream to write the table to. Caller is responsible for - * closing the stream after invoking {@link #finish()}. * @return {@code this} - * @throws java.io.IOException - * if reftable header cannot be written. */ - public ReftableWriter begin(OutputStream os) throws IOException { + public ReftableWriter begin() { + if (out != null) { + throw new IllegalStateException("begin() called twice.");//$NON-NLS-1$ + } + refBlockSize = config.getRefBlockSize(); logBlockSize = config.getLogBlockSize(); restartInterval = config.getRestartInterval(); @@ -212,7 +219,7 @@ public class ReftableWriter { restartInterval = refBlockSize < (60 << 10) ? 16 : 64; } - out = new ReftableOutputStream(os, refBlockSize, alignBlocks); + out = new ReftableOutputStream(outputStream, refBlockSize, alignBlocks); refs = new Section(REF_BLOCK_TYPE); if (indexObjects) { obj2ref = new ObjectIdSubclassMap<>(); @@ -223,6 +230,7 @@ public class ReftableWriter { /** * Sort a collection of references and write them to the reftable. + * The input refs may not have duplicate names. * * @param refsToPack * references to sort and write. @@ -236,10 +244,16 @@ public class ReftableWriter { .map(r -> new RefEntry(r, maxUpdateIndex - minUpdateIndex)) .sorted(Entry::compare) .iterator(); + RefEntry last = null; while (itr.hasNext()) { RefEntry entry = itr.next(); + if (last != null && Entry.compare(last, entry) == 0) { + throwIllegalEntry(last, entry); + } + long blockPos = refs.write(entry); indexRef(entry.ref, blockPos); + last = entry; } return this; } @@ -288,7 +302,7 @@ public class ReftableWriter { private void throwIllegalEntry(Entry last, Entry now) { throw new IllegalArgumentException(MessageFormat.format( - JGitText.get().refTableRecordsMustIncrease, + JGitText.get().reftableRecordsMustIncrease, new String(last.key, UTF_8), new String(now.key, UTF_8))); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java index da98e3fadd..9c5423fb0e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java @@ -102,9 +102,8 @@ class RefTreeBatch extends BatchRefUpdate { if (isAtomic()) { ReceiveCommand.abort(getCommands()); return; - } else { - continue; } + continue; } } todo.add(new Command(rw, c)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java index 882b2d055b..39a67afae3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java @@ -82,9 +82,8 @@ public class NetscapeCookieFileCache { public static NetscapeCookieFileCache getInstance(HttpConfig config) { if (instance == null) { return new NetscapeCookieFileCache(config); - } else { - return instance; } + return instance; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java index c1e94a0a3e..ee6adeee98 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java @@ -384,9 +384,8 @@ public class OpenSshConfigFile { private static boolean isHostMatch(String pattern, String name) { if (pattern.startsWith("!")) { //$NON-NLS-1$ return !patternMatchesHost(pattern.substring(1), name); - } else { - return patternMatchesHost(pattern, name); } + return patternMatchesHost(pattern, name); } private static boolean patternMatchesHost(String pattern, String name) { @@ -399,10 +398,9 @@ public class OpenSshConfigFile { } fn.append(name); return fn.isMatch(); - } else { - // Not a pattern but a full host name - return pattern.equals(name); } + // Not a pattern but a full host name + return pattern.equals(name); } private static String dequote(String value) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java index 4f90e69008..24850ee44c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AnyObjectId.java @@ -302,10 +302,10 @@ public abstract class AnyObjectId implements Comparable<AnyObjectId> { /** {@inheritDoc} */ @Override public final boolean equals(Object o) { - if (o instanceof AnyObjectId) + if (o instanceof AnyObjectId) { return equals((AnyObjectId) o); - else - return false; + } + return false; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java index 96e50667b3..98a46f3e54 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java @@ -103,25 +103,29 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re private static File getSymRef(File workTree, File dotGit, FS fs) throws IOException { byte[] content = IO.readFully(dotGit); - if (!isSymRef(content)) + if (!isSymRef(content)) { throw new IOException(MessageFormat.format( JGitText.get().invalidGitdirRef, dotGit.getAbsolutePath())); + } int pathStart = 8; int lineEnd = RawParseUtils.nextLF(content, pathStart); while (content[lineEnd - 1] == '\n' || - (content[lineEnd - 1] == '\r' && SystemReader.getInstance().isWindows())) + (content[lineEnd - 1] == '\r' + && SystemReader.getInstance().isWindows())) { lineEnd--; - if (lineEnd == pathStart) + } + if (lineEnd == pathStart) { throw new IOException(MessageFormat.format( JGitText.get().invalidGitdirRef, dotGit.getAbsolutePath())); + } String gitdirPath = RawParseUtils.decode(content, pathStart, lineEnd); File gitdirFile = fs.resolve(workTree, gitdirPath); - if (gitdirFile.isAbsolute()) + if (gitdirFile.isAbsolute()) { return gitdirFile; - else - return new File(workTree, gitdirPath).getCanonicalFile(); + } + return new File(workTree, gitdirPath).getCanonicalFile(); } private FS fs; @@ -723,9 +727,8 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re .getAbsolutePath(), err.getMessage())); } return cfg; - } else { - return new Config(); } + return new Config(); } private File guessWorkTreeOrFail() throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java index be53c4b4f6..cad747bcff 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java @@ -232,9 +232,9 @@ public class BranchConfig { private String getRemoteOrDefault() { String remote = getRemote(); - if (remote == null) + if (remote == null) { return Constants.DEFAULT_REMOTE_NAME; - else - return remote; + } + return remote; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java index d26b7fd17d..2ef365399f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java @@ -1407,12 +1407,11 @@ public class Config { } trailingSpaces.append(cc); continue; - } else { - inLeadingSpace = false; - if (trailingSpaces != null) { - value.append(trailingSpaces); - trailingSpaces.setLength(0); - } + } + inLeadingSpace = false; + if (trailingSpaces != null) { + value.append(trailingSpaces); + trailingSpaces.setLength(0); } if ('\\' == c) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 078bf786f2..99512a6a4a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -149,6 +149,18 @@ public final class ConfigConstants { */ public static final String CONFIG_KEY_GPGSIGN = "gpgSign"; + /** + * The "hooksPath" key. + * @since 5.6 + */ + public static final String CONFIG_KEY_HOOKS_PATH = "hooksPath"; + + /** + * The "quotePath" key. + * @since 5.6 + */ + public static final String CONFIG_KEY_QUOTE_PATH = "quotePath"; + /** The "algorithm" key */ public static final String CONFIG_KEY_ALGORITHM = "algorithm"; @@ -432,6 +444,12 @@ public final class ConfigConstants { public static final String CONFIG_RENAMELIMIT_COPIES = "copies"; /** + * A "refStorage" value in the "extensions". + * @since 5.6.2 + */ + public static final String CONFIG_REF_STORAGE_REFTABLE = "reftable"; + + /** * The "renames" key in the "diff" section * @since 3.0 */ @@ -526,9 +544,24 @@ public final class ConfigConstants { */ public static final String CONFIG_KEY_MIN_RACY_THRESHOLD = "minRacyThreshold"; + + /** + * The "refStorage" key + * + * @since 5.6.2 + */ + public static final String CONFIG_KEY_REF_STORAGE = "refStorage"; + /** * The "jmx" section * @since 5.1.13 */ public static final String CONFIG_JMX_SECTION = "jmx"; + + /** + * The "extensions" section + * + * @since 5.6.2 + */ + public static final String CONFIG_EXTENSIONS_SECTION = "extensions"; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java index 9274fc6777..3c05cab862 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -281,6 +281,18 @@ public final class Constants { */ public static final String OBJECTS = "objects"; + /** + * Reftable folder name + * @since 5.6 + */ + public static final String REFTABLE = "reftable"; + + /** + * Reftable table list name. + * @since 5.6.2 + */ + public static final String TABLES_LIST = "tables.list"; + /** Info refs folder */ public static final String INFO_REFS = "info/refs"; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java index 98de3a91cc..0056ad7632 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CoreConfig.java @@ -60,7 +60,7 @@ public class CoreConfig { public static final Config.SectionParser<CoreConfig> KEY = CoreConfig::new; /** Permissible values for {@code core.autocrlf}. */ - public static enum AutoCRLF { + public enum AutoCRLF { /** Automatic CRLF->LF conversion is disabled. */ FALSE, @@ -78,45 +78,45 @@ public class CoreConfig { * * @since 4.3 */ - public static enum EOL { - /** checkin with LF, checkout with CRLF. */ + public enum EOL { + /** Check in with LF, check out with CRLF. */ CRLF, - /** checkin with LF, checkout without conversion. */ + /** Check in with LF, check out without conversion. */ LF, - /** use the platform's native line ending. */ + /** Use the platform's native line ending. */ NATIVE; } /** - * EOL stream conversion protocol + * EOL stream conversion protocol. * * @since 4.3 */ - public static enum EolStreamType { - /** convert to CRLF without binary detection */ + public enum EolStreamType { + /** Convert to CRLF without binary detection. */ TEXT_CRLF, - /** convert to LF without binary detection */ + /** Convert to LF without binary detection. */ TEXT_LF, - /** convert to CRLF with binary detection */ + /** Convert to CRLF with binary detection. */ AUTO_CRLF, - /** convert to LF with binary detection */ + /** Convert to LF with binary detection. */ AUTO_LF, - /** do not convert */ + /** Do not convert. */ DIRECT; } /** - * Permissible values for {@code core.checkstat} + * Permissible values for {@code core.checkstat}. * * @since 3.0 */ - public static enum CheckStat { + public enum CheckStat { /** * Only check the size and whole second part of time stamp when * comparing the stat info in the dircache with actual file stat info. @@ -130,11 +130,30 @@ public class CoreConfig { DEFAULT } + /** + * Permissible values for {@code core.logAllRefUpdates}. + * + * @since 5.6 + */ + public enum LogRefUpdates { + /** Don't create ref logs; default for bare repositories. */ + FALSE, + + /** + * Create ref logs for refs/heads/**, refs/remotes/**, refs/notes/**, + * and for HEAD. Default for non-bare repositories. + */ + TRUE, + + /** Create ref logs for all refs/** and for HEAD. */ + ALWAYS + } + private final int compression; private final int packIndexVersion; - private final boolean logAllRefUpdates; + private final LogRefUpdates logAllRefUpdates; private final String excludesfile; @@ -145,24 +164,27 @@ public class CoreConfig { * * @since 3.3 */ - public static enum SymLinks { - /** Checkout symbolic links as plain files */ + public enum SymLinks { + /** Check out symbolic links as plain files . */ FALSE, - /** Checkout symbolic links as links */ + + /** Check out symbolic links as links. */ TRUE } /** - * Options for hiding files whose names start with a period + * Options for hiding files whose names start with a period. * * @since 3.5 */ - public static enum HideDotFiles { - /** Do not hide .files */ + public enum HideDotFiles { + /** Do not hide .files. */ FALSE, - /** Hide add .files */ + + /** Hide add .files. */ TRUE, - /** Hide only .git */ + + /** Hide only .git. */ DOTGITONLY } @@ -171,8 +193,9 @@ public class CoreConfig { ConfigConstants.CONFIG_KEY_COMPRESSION, DEFAULT_COMPRESSION); packIndexVersion = rc.getInt(ConfigConstants.CONFIG_PACK_SECTION, ConfigConstants.CONFIG_KEY_INDEXVERSION, 2); - logAllRefUpdates = rc.getBoolean(ConfigConstants.CONFIG_CORE_SECTION, - ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true); + logAllRefUpdates = rc.getEnum(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, + LogRefUpdates.TRUE); excludesfile = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, null, ConfigConstants.CONFIG_KEY_EXCLUDESFILE); attributesfile = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, @@ -201,9 +224,14 @@ public class CoreConfig { * Whether to log all refUpdates * * @return whether to log all refUpdates + * @deprecated since 5.6; default value depends on whether the repository is + * bare. Use + * {@link Config#getEnum(String, String, String, Enum)} + * directly. */ + @Deprecated public boolean isLogAllRefUpdates() { - return logAllRefUpdates; + return !LogRefUpdates.FALSE.equals(logAllRefUpdates); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java index e865da83b1..23e8de0e35 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java @@ -134,11 +134,9 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter { throw new IllegalArgumentException( MessageFormat.format(JGitText.get().enumValueNotSupported3, section, subsection, name, value)); - } else { - throw new IllegalArgumentException( - MessageFormat.format(JGitText.get().enumValueNotSupported2, - section, name, value)); } + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().enumValueNotSupported2, section, name, value)); } /** {@inheritDoc} */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java index ce1eb597fc..cbb3e54903 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java @@ -108,7 +108,7 @@ public class IndexDiff { * @see IndexDiff#getConflictingStageStates() * @since 3.0 */ - public static enum StageState { + public enum StageState { /** * Exists in base, but neither in ours nor in theirs. */ @@ -243,11 +243,11 @@ public class IndexDiff { } } - private final static int TREE = 0; + private static final int TREE = 0; - private final static int INDEX = 1; + private static final int INDEX = 1; - private final static int WORKDIR = 2; + private static final int WORKDIR = 2; private final Repository repository; @@ -384,7 +384,32 @@ public class IndexDiff { * @throws java.io.IOException */ public boolean diff() throws IOException { - return diff(null, 0, 0, ""); //$NON-NLS-1$ + return diff(null); + } + + /** + * Run the diff operation. Until this is called, all lists will be empty. + * Use + * {@link #diff(ProgressMonitor, int, int, String, RepositoryBuilderFactory)} + * if a progress monitor is required. + * <p> + * The operation may create repositories for submodules using builders + * provided by the given {@code factory}, if any, and will also close these + * submodule repositories again. + * </p> + * + * @param factory + * the {@link RepositoryBuilderFactory} to use to create builders + * to create submodule repositories, if needed; if {@code null}, + * submodule repositories will be built using a plain + * {@link RepositoryBuilder}. + * @return if anything is different between index, tree, and workdir + * @throws java.io.IOException + * @since 5.6 + */ + public boolean diff(RepositoryBuilderFactory factory) + throws IOException { + return diff(null, 0, 0, "", factory); //$NON-NLS-1$ } /** @@ -410,6 +435,45 @@ public class IndexDiff { public boolean diff(final ProgressMonitor monitor, int estWorkTreeSize, int estIndexSize, final String title) throws IOException { + return diff(monitor, estWorkTreeSize, estIndexSize, title, null); + } + + /** + * Run the diff operation. Until this is called, all lists will be empty. + * <p> + * The operation may be aborted by the progress monitor. In that event it + * will report what was found before the cancel operation was detected. + * Callers should ignore the result if monitor.isCancelled() is true. If a + * progress monitor is not needed, callers should use {@link #diff()} + * instead. Progress reporting is crude and approximate and only intended + * for informing the user. + * </p> + * <p> + * The operation may create repositories for submodules using builders + * provided by the given {@code factory}, if any, and will also close these + * submodule repositories again. + * </p> + * + * @param monitor + * for reporting progress, may be null + * @param estWorkTreeSize + * number or estimated files in the working tree + * @param estIndexSize + * number of estimated entries in the cache + * @param title + * a {@link java.lang.String} object. + * @param factory + * the {@link RepositoryBuilderFactory} to use to create builders + * to create submodule repositories, if needed; if {@code null}, + * submodule repositories will be built using a plain + * {@link RepositoryBuilder}. + * @return if anything is different between index, tree, and workdir + * @throws java.io.IOException + * @since 5.6 + */ + public boolean diff(ProgressMonitor monitor, int estWorkTreeSize, + int estIndexSize, String title, RepositoryBuilderFactory factory) + throws IOException { dirCache = repository.readDirCache(); try (TreeWalk treeWalk = new TreeWalk(repository)) { @@ -535,64 +599,69 @@ public class IndexDiff { } if (ignoreSubmoduleMode != IgnoreSubmoduleMode.ALL) { - IgnoreSubmoduleMode localIgnoreSubmoduleMode = ignoreSubmoduleMode; - SubmoduleWalk smw = SubmoduleWalk.forIndex(repository); - while (smw.next()) { - try { - if (localIgnoreSubmoduleMode == null) - localIgnoreSubmoduleMode = smw.getModulesIgnore(); - if (IgnoreSubmoduleMode.ALL - .equals(localIgnoreSubmoduleMode)) - continue; - } catch (ConfigInvalidException e) { - throw new IOException(MessageFormat.format( - JGitText.get().invalidIgnoreParamSubmodule, - smw.getPath()), e); - } - try (Repository subRepo = smw.getRepository()) { - String subRepoPath = smw.getPath(); - if (subRepo != null) { - ObjectId subHead = subRepo.resolve("HEAD"); //$NON-NLS-1$ - if (subHead != null - && !subHead.equals(smw.getObjectId())) { - modified.add(subRepoPath); - recordFileMode(subRepoPath, FileMode.GITLINK); - } else if (ignoreSubmoduleMode != IgnoreSubmoduleMode.DIRTY) { - IndexDiff smid = submoduleIndexDiffs.get(smw - .getPath()); - if (smid == null) { - smid = new IndexDiff(subRepo, - smw.getObjectId(), - wTreeIt.getWorkingTreeIterator(subRepo)); - submoduleIndexDiffs.put(subRepoPath, smid); - } - if (smid.diff()) { - if (ignoreSubmoduleMode == IgnoreSubmoduleMode.UNTRACKED - && smid.getAdded().isEmpty() - && smid.getChanged().isEmpty() - && smid.getConflicting().isEmpty() - && smid.getMissing().isEmpty() - && smid.getModified().isEmpty() - && smid.getRemoved().isEmpty()) { - continue; - } + try (SubmoduleWalk smw = new SubmoduleWalk(repository)) { + smw.setTree(new DirCacheIterator(dirCache)); + smw.setBuilderFactory(factory); + while (smw.next()) { + IgnoreSubmoduleMode localIgnoreSubmoduleMode = ignoreSubmoduleMode; + try { + if (localIgnoreSubmoduleMode == null) + localIgnoreSubmoduleMode = smw.getModulesIgnore(); + if (IgnoreSubmoduleMode.ALL + .equals(localIgnoreSubmoduleMode)) + continue; + } catch (ConfigInvalidException e) { + throw new IOException(MessageFormat.format( + JGitText.get().invalidIgnoreParamSubmodule, + smw.getPath()), e); + } + try (Repository subRepo = smw.getRepository()) { + String subRepoPath = smw.getPath(); + if (subRepo != null) { + ObjectId subHead = subRepo.resolve("HEAD"); //$NON-NLS-1$ + if (subHead != null + && !subHead.equals(smw.getObjectId())) { modified.add(subRepoPath); recordFileMode(subRepoPath, FileMode.GITLINK); + } else if (localIgnoreSubmoduleMode != IgnoreSubmoduleMode.DIRTY) { + IndexDiff smid = submoduleIndexDiffs + .get(smw.getPath()); + if (smid == null) { + smid = new IndexDiff(subRepo, + smw.getObjectId(), + wTreeIt.getWorkingTreeIterator( + subRepo)); + submoduleIndexDiffs.put(subRepoPath, smid); + } + if (smid.diff(factory)) { + if (localIgnoreSubmoduleMode == IgnoreSubmoduleMode.UNTRACKED + && smid.getAdded().isEmpty() + && smid.getChanged().isEmpty() + && smid.getConflicting().isEmpty() + && smid.getMissing().isEmpty() + && smid.getModified().isEmpty() + && smid.getRemoved().isEmpty()) { + continue; + } + modified.add(subRepoPath); + recordFileMode(subRepoPath, + FileMode.GITLINK); + } } - } - } else if (missingSubmodules.remove(subRepoPath)) { - // If the directory is there and empty but the submodule - // repository in .git/modules doesn't exist yet it isn't - // "missing". - File gitDir = new File( - new File(repository.getDirectory(), - Constants.MODULES), - subRepoPath); - if (!gitDir.isDirectory()) { - File dir = SubmoduleWalk.getSubmoduleDirectory( - repository, subRepoPath); - if (dir.isDirectory() && !hasFiles(dir)) { - missing.remove(subRepoPath); + } else if (missingSubmodules.remove(subRepoPath)) { + // If the directory is there and empty but the + // submodule repository in .git/modules doesn't + // exist yet it isn't "missing". + File gitDir = new File( + new File(repository.getDirectory(), + Constants.MODULES), + subRepoPath); + if (!gitDir.isDirectory()) { + File dir = SubmoduleWalk.getSubmoduleDirectory( + repository, subRepoPath); + if (dir.isDirectory() && !hasFiles(dir)) { + missing.remove(subRepoPath); + } } } } @@ -602,16 +671,17 @@ public class IndexDiff { } // consume the remaining work - if (monitor != null) + if (monitor != null) { monitor.endTask(); + } ignored = indexDiffFilter.getIgnoredPaths(); if (added.isEmpty() && changed.isEmpty() && removed.isEmpty() && missing.isEmpty() && modified.isEmpty() - && untracked.isEmpty()) + && untracked.isEmpty()) { return false; - else - return true; + } + return true; } private boolean hasFiles(File directory) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/InflaterCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/InflaterCache.java index dd0f18e165..314bde2c84 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/InflaterCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/InflaterCache.java @@ -73,7 +73,7 @@ public class InflaterCache { return r != null ? r : new Inflater(false); } - private synchronized static Inflater getImpl() { + private static synchronized Inflater getImpl() { if (openInflaterCount > 0) { final Inflater r = inflaterCache[--openInflaterCount]; inflaterCache[openInflaterCount] = null; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java index 74c712c46c..8730d66ecc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java @@ -355,7 +355,7 @@ public class ObjectIdOwnerMap<V extends ObjectIdOwnerMap.Entry> } /** Type of entry stored in the {@link ObjectIdOwnerMap}. */ - public static abstract class Entry extends ObjectId { + public abstract static class Entry extends ObjectId { transient Entry next; /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java index 77fa1b2346..edbb4b1c86 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectInserter.java @@ -98,7 +98,7 @@ public abstract class ObjectInserter implements AutoCloseable { } /** Wraps a delegate ObjectInserter. */ - public static abstract class Filter extends ObjectInserter { + public abstract static class Filter extends ObjectInserter { /** @return delegate ObjectInserter to handle all processing. */ protected abstract ObjectInserter delegate(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java index 2e52f031cd..b4734210f7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectLoader.java @@ -333,7 +333,7 @@ public abstract class ObjectLoader { * * @since 4.10 */ - public static abstract class Filter extends ObjectLoader { + public abstract static class Filter extends ObjectLoader { /** * @return delegate ObjectLoader to handle all processing. * @since 4.10 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java index 700b9dbe85..6d544670b0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java @@ -293,9 +293,8 @@ public abstract class ObjectReader implements AutoCloseable { if (idItr.hasNext()) { cur = idItr.next(); return true; - } else { - return false; } + return false; } @Override @@ -383,9 +382,8 @@ public abstract class ObjectReader implements AutoCloseable { cur = idItr.next(); sz = getObjectSize(cur, OBJ_ANY); return true; - } else { - return false; } + return false; } @Override @@ -497,7 +495,7 @@ public abstract class ObjectReader implements AutoCloseable { * * @since 4.4 */ - public static abstract class Filter extends ObjectReader { + public abstract static class Filter extends ObjectReader { /** * @return delegate ObjectReader to handle all processing. * @since 4.4 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java index 4e235b05c0..f893fd1bf9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoLine.java @@ -58,7 +58,7 @@ public class RebaseTodoLine { * Describes rebase actions */ @SuppressWarnings("nls") - public static enum Action { + public enum Action { /** Use commit */ PICK("pick", "p"), @@ -105,7 +105,7 @@ public class RebaseTodoLine { * @param token * @return the Action */ - static public Action parse(String token) { + public static Action parse(String token) { for (Action action : Action.values()) { if (action.token.equals(token) || action.shortToken.equals(token)) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java index 4d9450e758..9b5a1fdc22 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java @@ -497,6 +497,20 @@ public abstract class RefDatabase { } /** + * If the ref database does not support fast inverse queries, it may + * be advantageous to build a complete SHA1 to ref map in advance for + * multiple uses. To let applications decide on this decision, + * this function indicates whether the inverse map is available. + * + * @return whether this RefDatabase supports fast inverse ref queries. + * @throws IOException on I/O problems. + * @since 5.6 + */ + public boolean hasFastTipsWithSha1() throws IOException { + return false; + } + + /** * Check if any refs exist in the ref database. * <p> * This uses the same definition of refs as {@link #getRefs()}. In diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java index eca15c032a..eb5e139546 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java @@ -66,7 +66,7 @@ public abstract class RefUpdate { * unknown values are failures, and may generally treat them the same as * {@link #REJECTED_OTHER_REASON}. */ - public static enum Result { + public enum Result { /** The ref update/delete has not been attempted by the caller. */ NOT_ATTEMPTED, @@ -827,7 +827,7 @@ public abstract class RefUpdate { * Handle the abstraction of storing a ref update. This is because both * updating and deleting of a ref have merge testing in common. */ - private static abstract class Store { + private abstract static class Store { abstract Result execute(Result status) throws IOException; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index 68866ea279..0e9cf58cf2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -503,9 +503,8 @@ public abstract class Repository implements AutoCloseable { if (resolved instanceof String) { final Ref ref = findRef((String) resolved); return ref != null ? ref.getLeaf().getObjectId() : null; - } else { - return (ObjectId) resolved; } + return (ObjectId) resolved; } } @@ -527,11 +526,12 @@ public abstract class Repository implements AutoCloseable { try (RevWalk rw = new RevWalk(this)) { rw.setRetainBody(true); Object resolved = resolve(rw, revstr); - if (resolved != null) - if (resolved instanceof String) + if (resolved != null) { + if (resolved instanceof String) { return (String) resolved; - else - return ((AnyObjectId) resolved).getName(); + } + return ((AnyObjectId) resolved).getName(); + } return null; } } @@ -760,15 +760,15 @@ public abstract class Repository implements AutoCloseable { if (name == null) throw new RevisionSyntaxException(revstr); } else if (time.matches("^-\\d+$")) { //$NON-NLS-1$ - if (name != null) + if (name != null) { throw new RevisionSyntaxException(revstr); - else { - String previousCheckout = resolveReflogCheckout(-Integer - .parseInt(time)); - if (ObjectId.isId(previousCheckout)) - rev = parseSimple(rw, previousCheckout); - else - name = previousCheckout; + } + String previousCheckout = resolveReflogCheckout( + -Integer.parseInt(time)); + if (ObjectId.isId(previousCheckout)) { + rev = parseSimple(rw, previousCheckout); + } else { + name = previousCheckout; } } else { if (name == null) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryBuilderFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryBuilderFactory.java new file mode 100644 index 0000000000..fc12516837 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryBuilderFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019, Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import java.util.function.Supplier; + +/** + * A factory for {@link BaseRepositoryBuilder}s. + * <p> + * Note that a {@link BaseRepositoryBuilder} should be used only once to build a + * repository. Otherwise subsequently built repositories may be built using + * settings made for earlier built repositories. + * </p> + * + * @since 5.6 + */ +public interface RepositoryBuilderFactory extends + Supplier<BaseRepositoryBuilder<? extends BaseRepositoryBuilder, ? extends Repository>> { + // Empty +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java index fa113bfc6b..5a6063e9ab 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java @@ -66,7 +66,7 @@ import org.slf4j.LoggerFactory; * Cache of active {@link org.eclipse.jgit.lib.Repository} instances. */ public class RepositoryCache { - private final static Logger LOG = LoggerFactory + private static final Logger LOG = LoggerFactory .getLogger(RepositoryCache.class); private static final RepositoryCache cache = new RepositoryCache(); @@ -476,7 +476,8 @@ public class RepositoryCache { public static boolean isGitRepository(File dir, FS fs) { return fs.resolve(dir, Constants.OBJECTS).exists() && fs.resolve(dir, "refs").exists() //$NON-NLS-1$ - && isValidHead(new File(dir, Constants.HEAD)); + && (fs.resolve(dir, Constants.REFTABLE).exists() + || isValidHead(new File(dir, Constants.HEAD))); } private static boolean isValidHead(File head) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java index 11db7c5998..f28334cb80 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgKeyLocator.java @@ -72,12 +72,14 @@ import org.bouncycastle.gpg.keybox.PublicKeyRingBlob; import org.bouncycastle.gpg.keybox.UserID; import org.bouncycastle.gpg.keybox.jcajce.JcaKeyBoxBuilder; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyFlags; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory; import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider; @@ -90,6 +92,7 @@ import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.errors.UnsupportedCredentialItem; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -190,19 +193,92 @@ class BouncyCastleGpgKeyLocator { } } - private boolean containsSigningKey(String userId) { - return userId.toLowerCase(Locale.ROOT) - .contains(signingKey.toLowerCase(Locale.ROOT)); + /** + * Checks whether a given OpenPGP {@code userId} matches a given + * {@code signingKeySpec}, which is supposed to have one of the formats + * defined by GPG. + * <p> + * Not all formats are supported; only formats starting with '=', '<', + * '@', and '*' are handled. Any other format results in a case-insensitive + * substring match. + * </p> + * + * @param userId + * of a key + * @param signingKeySpec + * GPG key identification + * @return whether the {@code userId} matches + * @see <a href= + * "https://www.gnupg.org/documentation/manuals/gnupg/Specify-a-User-ID.html">GPG + * Documentation: How to Specify a User ID</a> + */ + static boolean containsSigningKey(String userId, String signingKeySpec) { + if (StringUtils.isEmptyOrNull(userId) + || StringUtils.isEmptyOrNull(signingKeySpec)) { + return false; + } + String toMatch = signingKeySpec; + if (toMatch.startsWith("0x") && toMatch.trim().length() > 2) { //$NON-NLS-1$ + return false; // Explicit fingerprint + } + int command = toMatch.charAt(0); + switch (command) { + case '=': + case '<': + case '@': + case '*': + toMatch = toMatch.substring(1); + if (toMatch.isEmpty()) { + return false; + } + break; + default: + break; + } + switch (command) { + case '=': + return userId.equals(toMatch); + case '<': { + int begin = userId.indexOf('<'); + int end = userId.indexOf('>', begin + 1); + int stop = toMatch.indexOf('>'); + return begin >= 0 && end > begin + 1 && stop > 0 + && userId.substring(begin + 1, end) + .equals(toMatch.substring(0, stop)); + } + case '@': { + int begin = userId.indexOf('<'); + int end = userId.indexOf('>', begin + 1); + return begin >= 0 && end > begin + 1 + && userId.substring(begin + 1, end).contains(toMatch); + } + default: + if (toMatch.trim().isEmpty()) { + return false; + } + return userId.toLowerCase(Locale.ROOT) + .contains(toMatch.toLowerCase(Locale.ROOT)); + } + } + + private String toFingerprint(String keyId) { + if (keyId.startsWith("0x")) { //$NON-NLS-1$ + return keyId.substring(2); + } + return keyId; } private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob) throws IOException { - String keyId = signingKey.toLowerCase(Locale.ROOT); + String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT); + if (keyId.isEmpty()) { + return null; + } for (KeyInformation keyInfo : keyBlob.getKeyInformation()) { String fingerprint = Hex.toHexString(keyInfo.getFingerprint()) .toLowerCase(Locale.ROOT); if (fingerprint.endsWith(keyId)) { - return getFirstPublicKey(keyBlob); + return getPublicKey(keyBlob, keyInfo.getFingerprint()); } } return null; @@ -211,8 +287,8 @@ class BouncyCastleGpgKeyLocator { private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob) throws IOException { for (UserID userID : keyBlob.getUserIds()) { - if (containsSigningKey(userID.getUserIDAsString())) { - return getFirstPublicKey(keyBlob); + if (containsSigningKey(userID.getUserIDAsString(), signingKey)) { + return getSigningPublicKey(keyBlob); } } return null; @@ -444,7 +520,7 @@ class BouncyCastleGpgKeyLocator { PGPUtil.getDecoderStream(new BufferedInputStream(in)), new JcaKeyFingerprintCalculator()); - String keyId = signingkey.toLowerCase(Locale.ROOT); + String keyId = toFingerprint(signingkey).toLowerCase(Locale.ROOT); Iterator<PGPSecretKeyRing> keyrings = pgpSec.getKeyRings(); while (keyrings.hasNext()) { PGPSecretKeyRing keyRing = keyrings.next(); @@ -462,7 +538,7 @@ class BouncyCastleGpgKeyLocator { Iterator<String> userIDs = key.getUserIDs(); while (userIDs.hasNext()) { String userId = userIDs.next(); - if (containsSigningKey(userId)) { + if (containsSigningKey(userId, signingKey)) { return key; } } @@ -490,7 +566,7 @@ class BouncyCastleGpgKeyLocator { new BufferedInputStream(in), new JcaKeyFingerprintCalculator()); - String keyId = signingKey.toLowerCase(Locale.ROOT); + String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT); Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings(); while (keyrings.hasNext()) { PGPPublicKeyRing keyRing = keyrings.next(); @@ -507,7 +583,7 @@ class BouncyCastleGpgKeyLocator { Iterator<String> userIDs = key.getUserIDs(); while (userIDs.hasNext()) { String userId = userIDs.next(); - if (containsSigningKey(userId)) { + if (containsSigningKey(userId, signingKey)) { return key; } } @@ -517,9 +593,42 @@ class BouncyCastleGpgKeyLocator { return null; } - private PGPPublicKey getFirstPublicKey(KeyBlob keyBlob) throws IOException { - return ((PublicKeyRingBlob) keyBlob).getPGPPublicKeyRing() - .getPublicKey(); + private PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint) + throws IOException { + return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing() + .getPublicKey(fingerprint); + } + + private PGPPublicKey getSigningPublicKey(KeyBlob blob) throws IOException { + PGPPublicKey masterKey = null; + Iterator<PGPPublicKey> keys = ((PublicKeyRingBlob) blob) + .getPGPPublicKeyRing().getPublicKeys(); + while (keys.hasNext()) { + PGPPublicKey key = keys.next(); + // only consider keys that have the [S] usage flag set + if (isSigningKey(key)) { + if (key.isMasterKey()) { + masterKey = key; + } else { + return key; + } + } + } + // return the master key if no other signing key was found or null if + // the master key did not have the signing flag set + return masterKey; + } + + private boolean isSigningKey(PGPPublicKey key) { + Iterator signatures = key.getSignatures(); + while (signatures.hasNext()) { + PGPSignature sig = (PGPSignature) signatures.next(); + if ((sig.getHashedSubPackets().getKeyFlags() + & PGPKeyFlags.CAN_SIGN) > 0) { + return true; + } + } + return false; } private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java index cfe0931b47..cfa67eefdc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/BouncyCastleGpgSigner.java @@ -115,7 +115,7 @@ public class BouncyCastleGpgSigner extends GpgSigner { NoSuchAlgorithmException, NoSuchProviderException, PGPException, URISyntaxException { if (gpgSigningKey == null || gpgSigningKey.isEmpty()) { - gpgSigningKey = committer.getEmailAddress(); + gpgSigningKey = '<' + committer.getEmailAddress() + '>'; } BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java index a77cb4ffb9..3ff38dc331 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeAlgorithm.java @@ -83,7 +83,7 @@ public final class MergeAlgorithm { // An special edit which acts as a sentinel value by marking the end the // list of edits - private final static Edit END_EDIT = new Edit(Integer.MAX_VALUE, + private static final Edit END_EDIT = new Edit(Integer.MAX_VALUE, Integer.MAX_VALUE); @SuppressWarnings("ReferenceEquality") diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeConfig.java index cdbe3cd26c..12f353e0da 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeConfig.java @@ -166,10 +166,10 @@ public class MergeConfig { String mergeOptions = config.getString( ConfigConstants.CONFIG_BRANCH_SECTION, branch, ConfigConstants.CONFIG_KEY_MERGEOPTIONS); - if (mergeOptions != null) + if (mergeOptions != null) { return mergeOptions.split("\\s"); //$NON-NLS-1$ - else - return new String[0]; + } + return new String[0]; } private static class MergeConfigSectionParser implements @@ -188,10 +188,10 @@ public class MergeConfig { @Override public boolean equals(Object obj) { - if (obj instanceof MergeConfigSectionParser) + if (obj instanceof MergeConfigSectionParser) { return branch.equals(((MergeConfigSectionParser) obj).branch); - else - return false; + } + return false; } @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java index ca0e18a0ef..ca2f37abee 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java @@ -153,15 +153,16 @@ public class MergeMessageFormatter { private static void addConflictsMessage(List<String> conflictingPaths, StringBuilder sb) { sb.append("Conflicts:\n"); //$NON-NLS-1$ - for (String conflictingPath : conflictingPaths) + for (String conflictingPath : conflictingPaths) { sb.append('\t').append(conflictingPath).append('\n'); + } } private static String joinNames(List<String> names, String singular, String plural) { - if (names.size() == 1) + if (names.size() == 1) { return singular + " " + names.get(0); //$NON-NLS-1$ - else - return plural + " " + StringUtils.join(names, ", ", " and "); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + return plural + " " + StringUtils.join(names, ", ", " and "); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index 0b423fb5d4..34b521e85f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -143,7 +143,7 @@ public class ResolveMerger extends ThreeWayMerger { * * @since 3.0 */ - protected String commitNames[]; + protected String[] commitNames; /** * Index of the base tree within the {@link #tw tree walk}. @@ -652,42 +652,40 @@ public class ResolveMerger extends ThreeWayMerger { keep(ourDce); // no checkout needed! return true; - } else { - // same content but different mode on OURS and THEIRS. - // Try to merge the mode and report an error if this is - // not possible. - int newMode = mergeFileModes(modeB, modeO, modeT); - if (newMode != FileMode.MISSING.getBits()) { - if (newMode == modeO) - // ours version is preferred - keep(ourDce); - else { - // the preferred version THEIRS has a different mode - // than ours. Check it out! - if (isWorktreeDirty(work, ourDce)) - return false; - // we know about length and lastMod only after we have written the new content. - // This will happen later. Set these values to 0 for know. - DirCacheEntry e = add(tw.getRawPath(), theirs, - DirCacheEntry.STAGE_0, EPOCH, 0); - addToCheckout(tw.getPathString(), e, attributes); - } - return true; + } + // same content but different mode on OURS and THEIRS. + // Try to merge the mode and report an error if this is + // not possible. + int newMode = mergeFileModes(modeB, modeO, modeT); + if (newMode != FileMode.MISSING.getBits()) { + if (newMode == modeO) { + // ours version is preferred + keep(ourDce); } else { - // FileModes are not mergeable. We found a conflict on modes. - // For conflicting entries we don't know lastModified and length. - add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); - add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); - add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, - 0); - unmergedPaths.add(tw.getPathString()); - mergeResults.put( - tw.getPathString(), - new MergeResult<>(Collections - .<RawText> emptyList())); + // the preferred version THEIRS has a different mode + // than ours. Check it out! + if (isWorktreeDirty(work, ourDce)) { + return false; + } + // we know about length and lastMod only after we have + // written the new content. + // This will happen later. Set these values to 0 for know. + DirCacheEntry e = add(tw.getRawPath(), theirs, + DirCacheEntry.STAGE_0, EPOCH, 0); + addToCheckout(tw.getPathString(), e, attributes); } return true; } + // FileModes are not mergeable. We found a conflict on modes. + // For conflicting entries we don't know lastModified and + // length. + add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); + add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0); + add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0); + unmergedPaths.add(tw.getPathString()); + mergeResults.put(tw.getPathString(), + new MergeResult<>(Collections.<RawText> emptyList())); + return true; } if (modeB == modeT && tw.idEqual(T_BASE, T_THEIRS)) { @@ -716,21 +714,20 @@ public class ResolveMerger extends ThreeWayMerger { addToCheckout(tw.getPathString(), e, attributes); } return true; - } else { - // we want THEIRS ... but THEIRS contains a folder or the - // deletion of the path. Delete what's in the working tree, - // which we know to be clean. - if (tw.getTreeCount() > T_FILE && tw.getRawMode(T_FILE) == 0) { - // Not present in working tree, so nothing to delete - return true; - } - if (modeT != 0 && modeT == modeB) { - // Base, ours, and theirs all contain a folder: don't delete - return true; - } - addDeletion(tw.getPathString(), nonTree(modeO), attributes); + } + // we want THEIRS ... but THEIRS contains a folder or the + // deletion of the path. Delete what's in the working tree, + // which we know to be clean. + if (tw.getTreeCount() > T_FILE && tw.getRawMode(T_FILE) == 0) { + // Not present in working tree, so nothing to delete return true; } + if (modeT != 0 && modeT == modeB) { + // Base, ours, and theirs all contain a folder: don't delete + return true; + } + addDeletion(tw.getPathString(), nonTree(modeO), attributes); + return true; } if (tw.isSubtree()) { @@ -1310,10 +1307,9 @@ public class ResolveMerger extends ThreeWayMerger { if (getUnmergedPaths().isEmpty() && !failed()) { resultTree = dircache.writeTree(getObjectInserter()); return true; - } else { - resultTree = null; - return false; } + resultTree = null; + return false; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java index 2fc0f4f073..d56e5c0c1e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ThreeWayMerger.java @@ -143,17 +143,17 @@ public abstract class ThreeWayMerger extends Merger { * @throws java.io.IOException */ protected AbstractTreeIterator mergeBase() throws IOException { - if (baseTree != null) + if (baseTree != null) { return openTree(baseTree); + } RevCommit baseCommit = (baseCommitId != null) ? walk .parseCommit(baseCommitId) : getBaseCommit(sourceCommits[0], sourceCommits[1]); if (baseCommit == null) { baseCommitId = null; return new EmptyTreeIterator(); - } else { - baseCommitId = baseCommit.toObjectId(); - return openTree(baseCommit.getTree()); } + baseCommitId = baseCommit.toObjectId(); + return openTree(baseCommit.getTree()); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java b/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java index 375cd32041..d9fb1b3a00 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/nls/NLS.java @@ -133,8 +133,8 @@ public class NLS { return b.get(type); } - final private Locale locale; - final private ConcurrentHashMap<Class, TranslationBundle> map = new ConcurrentHashMap<>(); + private final Locale locale; + private final ConcurrentHashMap<Class, TranslationBundle> map = new ConcurrentHashMap<>(); private NLS(Locale locale) { this.locale = locale; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java index 7827a9aa05..c1616b3ed8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java @@ -167,10 +167,10 @@ class FanoutBucket extends InMemoryNoteBucket { @Override public Note next() { - if (hasNext()) + if (hasNext()) { return itr.next(); - else - throw new NoSuchElementException(); + } + throw new NoSuchElementException(); } @Override @@ -214,30 +214,31 @@ class FanoutBucket extends InMemoryNoteBucket { NoteBucket b = table[cell]; if (b == null) { - if (noteData == null) + if (noteData == null) { return this; + } LeafBucket n = new LeafBucket(prefixLen + 2); table[cell] = n.set(noteOn, noteData, or); cnt++; return this; - } else { - NoteBucket n = b.set(noteOn, noteData, or); - if (n == null) { - table[cell] = null; - cnt--; + } + NoteBucket n = b.set(noteOn, noteData, or); + if (n == null) { + table[cell] = null; + cnt--; - if (cnt == 0) - return null; + if (cnt == 0) { + return null; + } - return contractIfTooSmall(noteOn, or); + return contractIfTooSmall(noteOn, or); - } else if (n != b) { - table[cell] = n; - } - return this; + } else if (n != b) { + table[cell] = n; } + return this; } InMemoryNoteBucket contractIfTooSmall(AnyObjectId noteOn, ObjectReader or) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java index 6723b6309c..0fa2a6306c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/LeafBucket.java @@ -129,10 +129,10 @@ class LeafBucket extends InMemoryNoteBucket { @Override public Note next() { - if (hasNext()) + if (hasNext()) { return notes[idx++]; - else - throw new NoSuchElementException(); + } + throw new NoSuchElementException(); } @Override @@ -156,25 +156,23 @@ class LeafBucket extends InMemoryNoteBucket { notes[p].setData(noteData.copy()); return this; - } else { - System.arraycopy(notes, p + 1, notes, p, cnt - p - 1); - cnt--; - return 0 < cnt ? this : null; } + System.arraycopy(notes, p + 1, notes, p, cnt - p - 1); + cnt--; + return 0 < cnt ? this : null; } else if (noteData != null) { if (shouldSplit()) { return split().set(noteOn, noteData, or); - - } else { - growIfFull(); - p = -(p + 1); - if (p < cnt) - System.arraycopy(notes, p, notes, p + 1, cnt - p); - notes[p] = new Note(noteOn, noteData.copy()); - cnt++; - return this; } + growIfFull(); + p = -(p + 1); + if (p < cnt) { + System.arraycopy(notes, p, notes, p + 1, cnt - p); + } + notes[p] = new Note(noteOn, noteData.copy()); + cnt++; + return this; } else { return this; @@ -234,12 +232,10 @@ class LeafBucket extends InMemoryNoteBucket { InMemoryNoteBucket append(Note note) { if (shouldSplit()) { return split().append(note); - - } else { - growIfFull(); - notes[cnt++] = note; - return this; } + growIfFull(); + notes[cnt++] = note; + return this; } private void growIfFull() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java index cbef61338f..e4eef433d8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMap.java @@ -278,10 +278,10 @@ public class NoteMap implements Iterable<Note> { public byte[] getCachedBytes(AnyObjectId id, int sizeLimit) throws LargeObjectException, MissingObjectException, IOException { ObjectId dataId = get(id); - if (dataId != null) + if (dataId != null) { return reader.open(dataId).getCachedBytes(sizeLimit); - else - return null; + } + return null; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java index ba7223b8f0..6ff1402900 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteMapMerger.java @@ -307,10 +307,10 @@ public class NoteMapMerger { private static InMemoryNoteBucket addIfNotNull(InMemoryNoteBucket result, Note note) { - if (note != null) + if (note != null) { return result.append(note); - else - return result; + } + return result; } private NonNoteEntry mergeNonNotes(NonNoteEntry baseList, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteParser.java index 8ef3af10ad..7dfc47deb0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NoteParser.java @@ -181,9 +181,8 @@ final class NoteParser extends CanonicalTreeParser { } catch (ArrayIndexOutOfBoundsException notHex) { return -1; } - } else { - return -1; } + return -1; } private void storeNonNote() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/BinaryHunk.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/BinaryHunk.java index 95391ec565..c1ee701cf1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/BinaryHunk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/BinaryHunk.java @@ -58,7 +58,7 @@ public class BinaryHunk { private static final byte[] DELTA = encodeASCII("delta "); //$NON-NLS-1$ /** Type of information stored in a binary hunk. */ - public static enum Type { + public enum Type { /** The full content is stored, deflated. */ LITERAL_DEFLATED, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java index 74eec81209..244f80483a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java @@ -58,7 +58,7 @@ import org.eclipse.jgit.util.MutableInteger; * Hunk header for a hunk appearing in a "diff --cc" style patch. */ public class CombinedHunkHeader extends HunkHeader { - private static abstract class CombinedOldImage extends OldImage { + private abstract static class CombinedOldImage extends OldImage { int nContext; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java index 1f4beb017f..959109c2e7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java @@ -104,7 +104,7 @@ public class FileHeader extends DiffEntry { static final byte[] NEW_NAME = encodeASCII("+++ "); //$NON-NLS-1$ /** Type of patch used by this file. */ - public static enum PatchType { + public enum PatchType { /** A traditional unified diff style patch of a text file. */ UNIFIED, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java index 10ea778343..389668733a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java @@ -54,7 +54,7 @@ import org.eclipse.jgit.util.RawParseUtils; */ public class FormatError { /** Classification of an error. */ - public static enum Severity { + public enum Severity { /** The error is unexpected, but can be worked around. */ WARNING, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java index 45508ce027..2bb45c55dc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java @@ -391,11 +391,10 @@ public class PlotCommitList<L extends PlotLane> extends return pos.intValue(); } return positionsAllocated++; - } else { - final Integer min = freePositions.first(); - freePositions.remove(min); - return min.intValue(); } + final Integer min = freePositions.first(); + freePositions.remove(min); + return min.intValue(); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java index ee18fe7c2f..19e40b562b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotWalk.java @@ -176,11 +176,10 @@ public class PlotWalk extends RevWalk { Collection<Ref> list = reverseRefMap.get(commitId); if (list == null) { return PlotCommit.NO_REFS; - } else { - Ref[] tags = list.toArray(new Ref[0]); - Arrays.sort(tags, new PlotRefComparator()); - return tags; } + Ref[] tags = list.toArray(new Ref[0]); + Arrays.sort(tags, new PlotRefComparator()); + return tags; } class PlotRefComparator implements Comparator<Ref> { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapCalculator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapCalculator.java deleted file mode 100644 index 14e95670aa..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmapCalculator.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2019, Google LLC. - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package org.eclipse.jgit.revwalk; - -import static java.util.Objects.requireNonNull; - -import java.io.IOException; - -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.internal.revwalk.AddToBitmapFilter; -import org.eclipse.jgit.lib.BitmapIndex; -import org.eclipse.jgit.lib.BitmapIndex.Bitmap; -import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder; -import org.eclipse.jgit.lib.ProgressMonitor; - -/** - * Calculate the bitmap indicating what other commits are reachable from certain - * commit. - * <p> - * This bitmap refers only to commits. For a bitmap with ALL objects reachable - * from certain object, see {@code BitmapWalker}. - */ -class BitmapCalculator { - - private final RevWalk walk; - private final BitmapIndex bitmapIndex; - - BitmapCalculator(RevWalk walk) throws IOException { - this.walk = walk; - this.bitmapIndex = requireNonNull( - walk.getObjectReader().getBitmapIndex()); - } - - /** - * Get the reachability bitmap from certain commit to other commits. - * <p> - * This will return a precalculated bitmap if available or walk building one - * until finding a precalculated bitmap (and returning the union). - * <p> - * Beware that the returned bitmap it is guaranteed to include ONLY the - * commits reachable from the initial commit. It COULD include other objects - * (because precalculated bitmaps have them) but caller shouldn't count on - * that. See {@link BitmapWalker} for a full reachability bitmap. - * - * @param start - * the commit. Use {@code walk.parseCommit(objectId)} to get this - * object from the id. - * @param pm - * progress monitor. Updated by one per commit browsed in the - * graph - * @return the bitmap of reachable commits (and maybe some extra objects) - * for the commit - * @throws MissingObjectException - * the supplied id doesn't exist - * @throws IncorrectObjectTypeException - * the supplied id doesn't refer to a commit or a tag - * @throws IOException - * if the walk cannot open a packfile or loose object - */ - BitmapBuilder getBitmap(RevCommit start, ProgressMonitor pm) - throws MissingObjectException, - IncorrectObjectTypeException, IOException { - Bitmap precalculatedBitmap = bitmapIndex.getBitmap(start); - if (precalculatedBitmap != null) { - return asBitmapBuilder(precalculatedBitmap); - } - - walk.reset(); - walk.sort(RevSort.TOPO); - walk.markStart(start); - // Unbounded walk. If the repo has bitmaps, it should bump into one at - // some point. - - BitmapBuilder bitmapResult = bitmapIndex.newBitmapBuilder(); - walk.setRevFilter(new AddToBitmapFilter(bitmapResult)); - while (walk.next() != null) { - // Iterate through all of the commits. The BitmapRevFilter does - // the work. - // - // filter.include returns true for commits that do not have - // a bitmap in bitmapIndex and are not reachable from a - // bitmap in bitmapIndex encountered earlier in the walk. - // Thus the number of commits returned by next() measures how - // much history was traversed without being able to make use - // of bitmaps. - pm.update(1); - } - - return bitmapResult; - } - - private BitmapBuilder asBitmapBuilder(Bitmap bitmap) { - return bitmapIndex.newBitmapBuilder().or(bitmap); - } -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java index 6e510f677b..bf831e3313 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BitmappedReachabilityChecker.java @@ -45,13 +45,18 @@ package org.eclipse.jgit.revwalk; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.BitmapIndex; +import org.eclipse.jgit.lib.BitmapIndex.Bitmap; import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder; -import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.revwalk.filter.RevFilter; /** * Checks the reachability using bitmaps. @@ -84,37 +89,108 @@ class BitmappedReachabilityChecker implements ReachabilityChecker { * Check all targets are reachable from the starters. * <p> * In this implementation, it is recommended to put the most popular - * starters (e.g. refs/heads tips) at the beginning of the collection + * starters (e.g. refs/heads tips) at the beginning. */ @Override public Optional<RevCommit> areAllReachable(Collection<RevCommit> targets, - Collection<RevCommit> starters) throws MissingObjectException, + Stream<RevCommit> starters) throws MissingObjectException, IncorrectObjectTypeException, IOException { - BitmapCalculator calculator = new BitmapCalculator(walk); - /** - * Iterate over starters bitmaps and remove targets as they become - * reachable. - * - * Building the total starters bitmap has the same cost (iterating over - * all starters adding the bitmaps) and this gives us the chance to - * shorcut the loop. - * - * This is based on the assuption that most of the starters will have - * the reachability bitmap precalculated. If many require a walk, the - * walk.reset() could start to take too much time. - */ List<RevCommit> remainingTargets = new ArrayList<>(targets); - for (RevCommit starter : starters) { - BitmapBuilder starterBitmap = calculator.getBitmap(starter, - NullProgressMonitor.INSTANCE); - remainingTargets.removeIf(starterBitmap::contains); - if (remainingTargets.isEmpty()) { - return Optional.empty(); + + walk.reset(); + walk.sort(RevSort.TOPO); + + // Filter emits only commits that are unreachable from previously + // visited commits. Internally it keeps a bitmap of everything + // reachable so far, which we use to discard reachable targets. + BitmapIndex repoBitmaps = walk.getObjectReader().getBitmapIndex(); + ReachedFilter reachedFilter = new ReachedFilter(repoBitmaps); + walk.setRevFilter(reachedFilter); + + Iterator<RevCommit> startersIter = starters.iterator(); + while (startersIter.hasNext()) { + walk.markStart(startersIter.next()); + while (walk.next() != null) { + remainingTargets.removeIf(reachedFilter::isReachable); + + if (remainingTargets.isEmpty()) { + return Optional.empty(); + } } + walk.reset(); } return Optional.of(remainingTargets.get(0)); } + /** + * This filter emits commits that were not bitmap-reachable from anything + * visited before. Or in other words, commits that add something (themselves + * or their bitmap) to the "reached" bitmap. + * + * Current progress can be queried via {@link #isReachable(RevCommit)}. + */ + private static class ReachedFilter extends RevFilter { + + private final BitmapIndex repoBitmaps; + private final BitmapBuilder reached; + + /** + * Create a filter that emits only previously unreachable commits. + * + * @param repoBitmaps + * bitmap index of the repo + */ + public ReachedFilter(BitmapIndex repoBitmaps) { + this.repoBitmaps = repoBitmaps; + this.reached = repoBitmaps.newBitmapBuilder(); + } + + /** {@inheritDoc} */ + @Override + public final boolean include(RevWalk walker, RevCommit cmit) { + Bitmap commitBitmap; + + if (reached.contains(cmit)) { + // already seen or included + dontFollow(cmit); + return false; + } + + if ((commitBitmap = repoBitmaps.getBitmap(cmit)) != null) { + reached.or(commitBitmap); + // Emit the commit because there are new contents in the bitmap + // but don't follow parents (they are already in the bitmap) + dontFollow(cmit); + return true; + } + + // No bitmaps, keep going + reached.addObject(cmit, Constants.OBJ_COMMIT); + return true; + } + + private static final void dontFollow(RevCommit cmit) { + for (RevCommit p : cmit.getParents()) { + p.add(RevFlag.SEEN); + } + } + + /** {@inheritDoc} */ + @Override + public final RevFilter clone() { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override + public final boolean requiresCommitBody() { + return false; + } + + boolean isReachable(RevCommit commit) { + return reached.contains(commit); + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java index bba3c5cff3..da9e75992f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PedestrianReachabilityChecker.java @@ -44,7 +44,9 @@ package org.eclipse.jgit.revwalk; import java.io.IOException; import java.util.Collection; +import java.util.Iterator; import java.util.Optional; +import java.util.stream.Stream; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -75,7 +77,7 @@ class PedestrianReachabilityChecker implements ReachabilityChecker { @Override public Optional<RevCommit> areAllReachable(Collection<RevCommit> targets, - Collection<RevCommit> starters) + Stream<RevCommit> starters) throws MissingObjectException, IncorrectObjectTypeException, IOException { walk.reset(); @@ -87,8 +89,9 @@ class PedestrianReachabilityChecker implements ReachabilityChecker { walk.markStart(target); } - for (RevCommit starter : starters) { - walk.markUninteresting(starter); + Iterator<RevCommit> iterator = starters.iterator(); + while (iterator.hasNext()) { + walk.markUninteresting(iterator.next()); } return Optional.ofNullable(walk.next()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java index 2ed06d1769..6a9c641318 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ReachabilityChecker.java @@ -45,6 +45,7 @@ package org.eclipse.jgit.revwalk; import java.io.IOException; import java.util.Collection; import java.util.Optional; +import java.util.stream.Stream; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -82,9 +83,43 @@ public interface ReachabilityChecker { * @throws IOException * if any of the underlying indexes or readers can not be * opened. + * + * @deprecated see {{@link #areAllReachable(Collection, Stream)} + */ + @Deprecated + default Optional<RevCommit> areAllReachable(Collection<RevCommit> targets, + Collection<RevCommit> starters) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + return areAllReachable(targets, starters.stream()); + } + + /** + * Check if all targets are reachable from the {@code starter} commits. + * <p> + * Caller should parse the objectIds (preferably with + * {@code walk.parseCommit()} and handle missing/incorrect type objects + * before calling this method. + * + * @param targets + * commits to reach. + * @param starters + * known starting points. + * @return An unreachable target if at least one of the targets is + * unreachable. An empty optional if all targets are reachable from + * the starters. + * + * @throws MissingObjectException + * if any of the incoming objects doesn't exist in the + * repository. + * @throws IncorrectObjectTypeException + * if any of the incoming objects is not a commit or a tag. + * @throws IOException + * if any of the underlying indexes or readers can not be + * opened. + * @since 5.6 */ Optional<RevCommit> areAllReachable(Collection<RevCommit> targets, - Collection<RevCommit> starters) + Stream<RevCommit> starters) throws MissingObjectException, IncorrectObjectTypeException, IOException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java index 2e26641ebc..b77407bb38 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java @@ -103,11 +103,15 @@ class RewriteGenerator extends Generator { final int nParents = pList.length; for (int i = 0; i < nParents; i++) { final RevCommit oldp = pList[i]; - if (firstParent && i > 0) { - c.parents = new RevCommit[] { rewrite(oldp) }; + final RevCommit newp = rewrite(oldp); + if (firstParent) { + if (newp == null) { + c.parents = RevCommit.NO_PARENTS; + } else { + c.parents = new RevCommit[] { newp }; + } return c; } - final RevCommit newp = rewrite(oldp); if (oldp != newp) { pList[i] = newp; rewrote = true; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java index a2c9ef6da4..e0325c29f9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java @@ -80,11 +80,11 @@ class TopoSortGenerator extends Generator { if (c == null) { break; } - for (int i = 0; i < c.parents.length; i++) { - if (firstParent && i > 0) { + for (RevCommit p : c.parents) { + p.inDegree++; + if (firstParent) { break; } - c.parents[i].inDegree++; } pending.add(c); } @@ -119,11 +119,7 @@ class TopoSortGenerator extends Generator { // All of our children have already produced, // so it is OK for us to produce now as well. // - for (int i = 0; i < c.parents.length; i++) { - if (firstParent && i > 0) { - break; - } - RevCommit p = c.parents[i]; + for (RevCommit p : c.parents) { if (--p.inDegree == 0 && (p.flags & TOPO_DELAY) != 0) { // This parent tried to come before us, but we are // his last child. unpop the parent so it goes right @@ -132,6 +128,9 @@ class TopoSortGenerator extends Generator { p.flags &= ~TOPO_DELAY; pending.unpop(p); } + if (firstParent) { + break; + } } return c; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java index f7c3218850..090d1e110c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java @@ -169,19 +169,19 @@ public class TreeRevFilter extends RevFilter { // c.flags |= rewriteFlag; return false; - } else { - // We have interesting items, but neither of the special - // cases denoted above. + } + + // We have interesting items, but neither of the special + // cases denoted above. + // + if (adds > 0 && tw.getFilter() instanceof FollowFilter) { + // One of the paths we care about was added in this + // commit. We need to update our filter to its older + // name, if we can discover it. Find out what that is. // - if (adds > 0 && tw.getFilter() instanceof FollowFilter) { - // One of the paths we care about was added in this - // commit. We need to update our filter to its older - // name, if we can discover it. Find out what that is. - // - updateFollowFilter(trees, ((FollowFilter) tw.getFilter()).cfg); - } - return true; + updateFollowFilter(trees, ((FollowFilter) tw.getFilter()).cfg); } + return true; } else if (nParents == 0) { // We have no parents to compare against. Consider us to be // REWRITE only if we have no paths matching our filter. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java index 5bb8153a58..f1ff9e6ba0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java @@ -77,7 +77,7 @@ import org.slf4j.LoggerFactory; * The configuration file that is stored in the file of the file system. */ public class FileBasedConfig extends StoredConfig { - private final static Logger LOG = LoggerFactory + private static final Logger LOG = LoggerFactory .getLogger(FileBasedConfig.class); private final File configFile; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java index e6e3d4fb12..645da0a068 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java @@ -271,6 +271,20 @@ public class PackStatistics { public long treesTraversed; /** + * Amount of packfile uris sent to the client to download via HTTP. + * + * @since 5.6 + */ + public long offloadedPackfiles; + + /** + * Total size (in bytes) offloaded to HTTP downloads. + * + * @since 5.6 + */ + public long offloadedPackfileSize; + + /** * Statistics about each object type in the pack (commits, tags, trees * and blobs.) */ @@ -598,6 +612,22 @@ public class PackStatistics { } /** + * @return amount of packfiles offloaded (sent as "packfile-uri")/ + * @since 5.6 + */ + public long getOffloadedPackfiles() { + return statistics.offloadedPackfiles; + } + + /** + * @return total size (in bytes) offloaded to HTTP downloads. + * @since 5.6 + */ + public long getOffloadedPackfilesSize() { + return statistics.offloadedPackfileSize; + } + + /** * Get total time spent processing this pack. * * @return total time spent processing this pack. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java index e5559dea09..2e5776d646 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java @@ -57,6 +57,7 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.BaseRepositoryBuilder; import org.eclipse.jgit.lib.BlobBasedConfig; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; @@ -66,6 +67,7 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryBuilder; +import org.eclipse.jgit.lib.RepositoryBuilderFactory; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.treewalk.AbstractTreeIterator; @@ -260,15 +262,41 @@ public class SubmoduleWalk implements AutoCloseable { */ public static Repository getSubmoduleRepository(final File parent, final String path, FS fs) throws IOException { + return getSubmoduleRepository(parent, path, fs, + new RepositoryBuilder()); + } + + /** + * Get submodule repository at path, using the specified file system + * abstraction and the specified builder + * + * @param parent + * {@link Repository} that contains the submodule + * @param path + * of the working tree of the submodule + * @param fs + * {@link FS} to use + * @param builder + * {@link BaseRepositoryBuilder} to use to build the submodule + * repository + * @return the {@link Repository} of the submodule, or {@code null} if it + * doesn't exist + * @throws IOException + * on errors + * @since 5.6 + */ + public static Repository getSubmoduleRepository(File parent, String path, + FS fs, BaseRepositoryBuilder<?, ? extends Repository> builder) + throws IOException { File subWorkTree = new File(parent, path); - if (!subWorkTree.isDirectory()) + if (!subWorkTree.isDirectory()) { return null; - File workTree = new File(parent, path); + } try { - return new RepositoryBuilder() // + return builder // .setMustExist(true) // .setFS(fs) // - .setWorkTree(workTree) // + .setWorkTree(subWorkTree) // .build(); } catch (RepositoryNotFoundException e) { return null; @@ -366,6 +394,8 @@ public class SubmoduleWalk implements AutoCloseable { private Map<String, String> pathToName; + private RepositoryBuilderFactory factory; + /** * Create submodule generator * @@ -639,7 +669,25 @@ public class SubmoduleWalk implements AutoCloseable { } /** - * The module name for the current submodule entry (used for the section name of .git/config) + * Sets the {@link RepositoryBuilderFactory} to use for creating submodule + * repositories. If none is set, a plain {@link RepositoryBuilder} is used. + * + * @param factory + * to set + * @since 5.6 + */ + public void setBuilderFactory(RepositoryBuilderFactory factory) { + this.factory = factory; + } + + private BaseRepositoryBuilder<?, ? extends Repository> getBuilder() { + return factory != null ? factory.get() : new RepositoryBuilder(); + } + + /** + * The module name for the current submodule entry (used for the section + * name of .git/config) + * * @since 4.10 * @return name */ @@ -735,6 +783,13 @@ public class SubmoduleWalk implements AutoCloseable { */ public IgnoreSubmoduleMode getModulesIgnore() throws IOException, ConfigInvalidException { + IgnoreSubmoduleMode mode = repoConfig.getEnum( + IgnoreSubmoduleMode.values(), + ConfigConstants.CONFIG_SUBMODULE_SECTION, getModuleName(), + ConfigConstants.CONFIG_KEY_IGNORE, null); + if (mode != null) { + return mode; + } lazyLoadModulesConfig(); return modulesConfig.getEnum(IgnoreSubmoduleMode.values(), ConfigConstants.CONFIG_SUBMODULE_SECTION, getModuleName(), @@ -748,7 +803,8 @@ public class SubmoduleWalk implements AutoCloseable { * @throws java.io.IOException */ public Repository getRepository() throws IOException { - return getSubmoduleRepository(repository, path); + return getSubmoduleRepository(repository.getWorkTree(), path, + repository.getFS(), getBuilder()); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AbstractAdvertiseRefsHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AbstractAdvertiseRefsHook.java index 4bf0d2692a..ed900121be 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AbstractAdvertiseRefsHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AbstractAdvertiseRefsHook.java @@ -67,7 +67,7 @@ public abstract class AbstractAdvertiseRefsHook implements AdvertiseRefsHook { /** {@inheritDoc} */ @Override - public void advertiseRefs(BaseReceivePack receivePack) + public void advertiseRefs(ReceivePack receivePack) throws ServiceMayNotContinueException { Map<String, Ref> refs = getAdvertisedRefs(receivePack.getRepository(), receivePack.getRevWalk()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java index 8512f2d9fe..eb1aef9ad7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHook.java @@ -51,8 +51,8 @@ public interface AdvertiseRefsHook { /** * A simple hook that advertises the default refs. * <p> - * The method implementations do nothing to preserve the default behavior; see - * {@link UploadPack#setAdvertisedRefs(java.util.Map)} and + * The method implementations do nothing to preserve the default behavior; + * see {@link UploadPack#setAdvertisedRefs(java.util.Map)} and * {@link ReceivePack#setAdvertisedRefs(java.util.Map,java.util.Set)}. */ AdvertiseRefsHook DEFAULT = new AdvertiseRefsHook() { @@ -62,7 +62,7 @@ public interface AdvertiseRefsHook { } @Override - public void advertiseRefs(BaseReceivePack receivePack) { + public void advertiseRefs(ReceivePack receivePack) { // Do nothing. } }; @@ -89,7 +89,8 @@ public interface AdvertiseRefsHook { * if necessary. * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException * abort; the message will be sent to the user. + * @since 5.6 */ - void advertiseRefs(BaseReceivePack receivePack) + void advertiseRefs(ReceivePack receivePack) throws ServiceMayNotContinueException; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHookChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHookChain.java index 12238a1f77..54c19783e3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHookChain.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AdvertiseRefsHookChain.java @@ -53,7 +53,7 @@ import java.util.List; * modify the results of the previous hooks in the chain by calling * {@link org.eclipse.jgit.transport.UploadPack#getAdvertisedRefs()}, or * {@link org.eclipse.jgit.transport.ReceivePack#getAdvertisedRefs()} or - * {@link org.eclipse.jgit.transport.BaseReceivePack#getAdvertisedObjects()}. + * {@link org.eclipse.jgit.transport.ReceivePack#getAdvertisedObjects()}. */ public class AdvertiseRefsHookChain implements AdvertiseRefsHook { private final AdvertiseRefsHook[] hooks; @@ -82,7 +82,7 @@ public class AdvertiseRefsHookChain implements AdvertiseRefsHook { /** {@inheritDoc} */ @Override - public void advertiseRefs(BaseReceivePack rp) + public void advertiseRefs(ReceivePack rp) throws ServiceMayNotContinueException { for (int i = 0; i < count; i++) hooks[i].advertiseRefs(rp); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java index 35ea35ecb8..12ade4d195 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java @@ -391,7 +391,7 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen refNameEnd = refLine.length(); } else if (refLine.startsWith("ng ")) { //$NON-NLS-1$ ok = false; - refNameEnd = refLine.indexOf(" ", 3); //$NON-NLS-1$ + refNameEnd = refLine.indexOf(' ', 3); } if (refNameEnd == -1) throw new PackProtocolException(MessageFormat.format(JGitText.get().unexpectedReportLine2 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java deleted file mode 100644 index 36a10cc6b9..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ /dev/null @@ -1,1971 +0,0 @@ -/* - * Copyright (C) 2008-2010, Google Inc. - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.eclipse.jgit.transport; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_ATOMIC; -import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_DELETE_REFS; -import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_OFS_DELTA; -import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_OPTIONS; -import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_QUIET; -import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_REPORT_STATUS; -import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SIDE_BAND_64K; -import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; -import static org.eclipse.jgit.transport.SideBandOutputStream.CH_DATA; -import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR; -import static org.eclipse.jgit.transport.SideBandOutputStream.CH_PROGRESS; -import static org.eclipse.jgit.transport.SideBandOutputStream.MAX_BUF; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jgit.annotations.Nullable; -import org.eclipse.jgit.errors.InvalidObjectIdException; -import org.eclipse.jgit.errors.LargeObjectException; -import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.errors.PackProtocolException; -import org.eclipse.jgit.errors.TooLargePackException; -import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.internal.storage.file.PackLock; -import org.eclipse.jgit.internal.submodule.SubmoduleValidator; -import org.eclipse.jgit.internal.submodule.SubmoduleValidator.SubmoduleValidationException; -import org.eclipse.jgit.internal.transport.parser.FirstCommand; -import org.eclipse.jgit.lib.AnyObjectId; -import org.eclipse.jgit.lib.BatchRefUpdate; -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.GitmoduleEntry; -import org.eclipse.jgit.lib.NullProgressMonitor; -import org.eclipse.jgit.lib.ObjectChecker; -import org.eclipse.jgit.lib.ObjectDatabase; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectIdSubclassMap; -import org.eclipse.jgit.lib.ObjectInserter; -import org.eclipse.jgit.lib.ObjectLoader; -import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.lib.ProgressMonitor; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.revwalk.ObjectWalk; -import org.eclipse.jgit.revwalk.RevBlob; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevFlag; -import org.eclipse.jgit.revwalk.RevObject; -import org.eclipse.jgit.revwalk.RevSort; -import org.eclipse.jgit.revwalk.RevTree; -import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.transport.PacketLineIn.InputOverLimitIOException; -import org.eclipse.jgit.transport.ReceiveCommand.Result; -import org.eclipse.jgit.util.io.InterruptTimer; -import org.eclipse.jgit.util.io.LimitedInputStream; -import org.eclipse.jgit.util.io.TimeoutInputStream; -import org.eclipse.jgit.util.io.TimeoutOutputStream; - -/** - * Base implementation of the side of a push connection that receives objects. - * <p> - * Contains high-level operations for initializing and closing streams, - * advertising refs, reading commands, and receiving and applying a pack. - * Subclasses compose these operations into full service implementations. - */ -public abstract class BaseReceivePack { - /** - * Data in the first line of a request, the line itself plus capabilities. - * - * @deprecated Use {@link FirstCommand} instead. - */ - @Deprecated - public static class FirstLine { - private final FirstCommand command; - - /** - * Parse the first line of a receive-pack request. - * - * @param line - * line from the client. - */ - public FirstLine(String line) { - command = FirstCommand.fromLine(line); - } - - /** @return non-capabilities part of the line. */ - public String getLine() { - return command.getLine(); - } - - /** @return capabilities parsed from the line. */ - public Set<String> getCapabilities() { - return command.getCapabilities(); - } - } - - /** Database we write the stored objects into. */ - final Repository db; - - /** Revision traversal support over {@link #db}. */ - final RevWalk walk; - - /** - * Is the client connection a bi-directional socket or pipe? - * <p> - * If true, this class assumes it can perform multiple read and write cycles - * with the client over the input and output streams. This matches the - * functionality available with a standard TCP/IP connection, or a local - * operating system or in-memory pipe. - * <p> - * If false, this class runs in a read everything then output results mode, - * making it suitable for single round-trip systems RPCs such as HTTP. - */ - private boolean biDirectionalPipe = true; - - /** Expecting data after the pack footer */ - private boolean expectDataAfterPackFooter; - - /** Should an incoming transfer validate objects? */ - private ObjectChecker objectChecker; - - /** Should an incoming transfer permit create requests? */ - private boolean allowCreates; - - /** Should an incoming transfer permit delete requests? */ - private boolean allowAnyDeletes; - private boolean allowBranchDeletes; - - /** Should an incoming transfer permit non-fast-forward requests? */ - private boolean allowNonFastForwards; - - /** Should an incoming transfer permit push options? **/ - private boolean allowPushOptions; - - /** - * Should the requested ref updates be performed as a single atomic - * transaction? - */ - private boolean atomic; - - private boolean allowOfsDelta; - private boolean allowQuiet = true; - - /** Identity to record action as within the reflog. */ - private PersonIdent refLogIdent; - - /** Hook used while advertising the refs to the client. */ - private AdvertiseRefsHook advertiseRefsHook; - - /** Filter used while advertising the refs to the client. */ - RefFilter refFilter; - - /** Timeout in seconds to wait for client interaction. */ - private int timeout; - - /** Timer to manage {@link #timeout}. */ - private InterruptTimer timer; - - private TimeoutInputStream timeoutIn; - - // Original stream passed to init(), since rawOut may be wrapped in a - // sideband. - private OutputStream origOut; - - /** Raw input stream. */ - protected InputStream rawIn; - - /** Raw output stream. */ - protected OutputStream rawOut; - - /** Optional message output stream. */ - protected OutputStream msgOut; - private SideBandOutputStream errOut; - - /** Packet line input stream around {@link #rawIn}. */ - protected PacketLineIn pckIn; - - /** Packet line output stream around {@link #rawOut}. */ - protected PacketLineOut pckOut; - - private final MessageOutputWrapper msgOutWrapper = new MessageOutputWrapper(); - - private PackParser parser; - - /** The refs we advertised as existing at the start of the connection. */ - Map<String, Ref> refs; - - /** All SHA-1s shown to the client, which can be possible edges. */ - Set<ObjectId> advertisedHaves; - - /** Capabilities requested by the client. */ - private Set<String> enabledCapabilities; - String userAgent; - private Set<ObjectId> clientShallowCommits; - private List<ReceiveCommand> commands; - private long maxCommandBytes; - private long maxDiscardBytes; - - private StringBuilder advertiseError; - - /** If {@link BasePackPushConnection#CAPABILITY_SIDE_BAND_64K} is enabled. */ - private boolean sideBand; - - private boolean quiet; - - /** Lock around the received pack file, while updating refs. */ - private PackLock packLock; - - private boolean checkReferencedIsReachable; - - /** Git object size limit */ - private long maxObjectSizeLimit; - - /** Total pack size limit */ - private long maxPackSizeLimit = -1; - - /** The size of the received pack, including index size */ - private Long packSize; - - private PushCertificateParser pushCertificateParser; - private SignedPushConfig signedPushConfig; - PushCertificate pushCert; - private ReceivedPackStatistics stats; - - /** - * Get the push certificate used to verify the pusher's identity. - * <p> - * Only valid after commands are read from the wire. - * - * @return the parsed certificate, or null if push certificates are disabled - * or no cert was presented by the client. - * @since 4.1 - * @deprecated use {@link ReceivePack#getPushCertificate}. - */ - @Deprecated - public abstract PushCertificate getPushCertificate(); - - /** - * Set the push certificate used to verify the pusher's identity. - * <p> - * Should only be called if reconstructing an instance without going through - * the normal {@link #recvCommands()} flow. - * - * @param cert - * the push certificate to set. - * @since 4.1 - * @deprecated use {@link ReceivePack#setPushCertificate(PushCertificate)}. - */ - @Deprecated - public abstract void setPushCertificate(PushCertificate cert); - - /** - * Create a new pack receive for an open repository. - * - * @param into - * the destination repository. - */ - protected BaseReceivePack(Repository into) { - db = into; - walk = new RevWalk(db); - walk.setRetainBody(false); - - TransferConfig tc = db.getConfig().get(TransferConfig.KEY); - objectChecker = tc.newReceiveObjectChecker(); - - ReceiveConfig rc = db.getConfig().get(ReceiveConfig::new); - allowCreates = rc.allowCreates; - allowAnyDeletes = true; - allowBranchDeletes = rc.allowDeletes; - allowNonFastForwards = rc.allowNonFastForwards; - allowOfsDelta = rc.allowOfsDelta; - allowPushOptions = rc.allowPushOptions; - maxCommandBytes = rc.maxCommandBytes; - maxDiscardBytes = rc.maxDiscardBytes; - advertiseRefsHook = AdvertiseRefsHook.DEFAULT; - refFilter = RefFilter.DEFAULT; - advertisedHaves = new HashSet<>(); - clientShallowCommits = new HashSet<>(); - signedPushConfig = rc.signedPush; - } - - /** Configuration for receive operations. */ - protected static class ReceiveConfig { - final boolean allowCreates; - final boolean allowDeletes; - final boolean allowNonFastForwards; - final boolean allowOfsDelta; - final boolean allowPushOptions; - final long maxCommandBytes; - final long maxDiscardBytes; - final SignedPushConfig signedPush; - - ReceiveConfig(Config config) { - allowCreates = true; - allowDeletes = !config.getBoolean("receive", "denydeletes", false); //$NON-NLS-1$ //$NON-NLS-2$ - allowNonFastForwards = !config.getBoolean("receive", //$NON-NLS-1$ - "denynonfastforwards", false); //$NON-NLS-1$ - allowOfsDelta = config.getBoolean("repack", "usedeltabaseoffset", //$NON-NLS-1$ //$NON-NLS-2$ - true); - allowPushOptions = config.getBoolean("receive", "pushoptions", //$NON-NLS-1$ //$NON-NLS-2$ - false); - maxCommandBytes = config.getLong("receive", //$NON-NLS-1$ - "maxCommandBytes", //$NON-NLS-1$ - 3 << 20); - maxDiscardBytes = config.getLong("receive", //$NON-NLS-1$ - "maxCommandDiscardBytes", //$NON-NLS-1$ - -1); - signedPush = SignedPushConfig.KEY.parse(config); - } - } - - /** - * Output stream that wraps the current {@link #msgOut}. - * <p> - * We don't want to expose {@link #msgOut} directly because it can change - * several times over the course of a session. - */ - class MessageOutputWrapper extends OutputStream { - @Override - public void write(int ch) { - if (msgOut != null) { - try { - msgOut.write(ch); - } catch (IOException e) { - // Ignore write failures. - } - } - } - - @Override - public void write(byte[] b, int off, int len) { - if (msgOut != null) { - try { - msgOut.write(b, off, len); - } catch (IOException e) { - // Ignore write failures. - } - } - } - - @Override - public void write(byte[] b) { - write(b, 0, b.length); - } - - @Override - public void flush() { - if (msgOut != null) { - try { - msgOut.flush(); - } catch (IOException e) { - // Ignore write failures. - } - } - } - } - - /** - * Get the process name used for pack lock messages. - * - * @return the process name used for pack lock messages. - */ - protected abstract String getLockMessageProcessName(); - - /** - * Get the repository this receive completes into. - * - * @return the repository this receive completes into. - * @deprecated use {@link ReceivePack#getRepository} - */ - @Deprecated - public abstract Repository getRepository(); - - /** - * Get the RevWalk instance used by this connection. - * - * @return the RevWalk instance used by this connection. - * @deprecated use {@link ReceivePack#getRevWalk} - */ - @Deprecated - public abstract RevWalk getRevWalk(); - - /** - * Get refs which were advertised to the client. - * - * @return all refs which were advertised to the client, or null if - * {@link #setAdvertisedRefs(Map, Set)} has not been called yet. - * @deprecated use {@link ReceivePack#getAdvertisedRefs} - */ - @Deprecated - public abstract Map<String, Ref> getAdvertisedRefs(); - - /** - * Set the refs advertised by this ReceivePack. - * <p> - * Intended to be called from a - * {@link org.eclipse.jgit.transport.PreReceiveHook}. - * - * @param allRefs - * explicit set of references to claim as advertised by this - * ReceivePack instance. This overrides any references that may - * exist in the source repository. The map is passed to the - * configured {@link #getRefFilter()}. If null, assumes all refs - * were advertised. - * @param additionalHaves - * explicit set of additional haves to claim as advertised. If - * null, assumes the default set of additional haves from the - * repository. - * @deprecated use {@link ReceivePack#setAdvertisedRefs} - */ - @Deprecated - public abstract void setAdvertisedRefs(Map<String, Ref> allRefs, Set<ObjectId> additionalHaves); - - /** - * Get objects advertised to the client. - * - * @return the set of objects advertised to the as present in this repository, - * or null if {@link #setAdvertisedRefs(Map, Set)} has not been called - * yet. - */ - public final Set<ObjectId> getAdvertisedObjects() { - return advertisedHaves; - } - - /** - * Whether this instance will validate all referenced, but not supplied by - * the client, objects are reachable from another reference. - * - * @return true if this instance will validate all referenced, but not - * supplied by the client, objects are reachable from another - * reference. - */ - public boolean isCheckReferencedObjectsAreReachable() { - return checkReferencedIsReachable; - } - - /** - * Validate all referenced but not supplied objects are reachable. - * <p> - * If enabled, this instance will verify that references to objects not - * contained within the received pack are already reachable through at least - * one other reference displayed as part of {@link #getAdvertisedRefs()}. - * <p> - * This feature is useful when the application doesn't trust the client to - * not provide a forged SHA-1 reference to an object, in an attempt to - * access parts of the DAG that they aren't allowed to see and which have - * been hidden from them via the configured - * {@link org.eclipse.jgit.transport.AdvertiseRefsHook} or - * {@link org.eclipse.jgit.transport.RefFilter}. - * <p> - * Enabling this feature may imply at least some, if not all, of the same - * functionality performed by {@link #setCheckReceivedObjects(boolean)}. - * Applications are encouraged to enable both features, if desired. - * - * @param b - * {@code true} to enable the additional check. - */ - public void setCheckReferencedObjectsAreReachable(boolean b) { - this.checkReferencedIsReachable = b; - } - - /** - * Whether this class expects a bi-directional pipe opened between the - * client and itself. - * - * @return true if this class expects a bi-directional pipe opened between - * the client and itself. The default is true. - */ - public boolean isBiDirectionalPipe() { - return biDirectionalPipe; - } - - /** - * Whether this class will assume the socket is a fully bidirectional pipe - * between the two peers and takes advantage of that by first transmitting - * the known refs, then waiting to read commands. - * - * @param twoWay - * if true, this class will assume the socket is a fully - * bidirectional pipe between the two peers and takes advantage - * of that by first transmitting the known refs, then waiting to - * read commands. If false, this class assumes it must read the - * commands before writing output and does not perform the - * initial advertising. - */ - public void setBiDirectionalPipe(boolean twoWay) { - biDirectionalPipe = twoWay; - } - - /** - * Whether there is data expected after the pack footer. - * - * @return {@code true} if there is data expected after the pack footer. - */ - public boolean isExpectDataAfterPackFooter() { - return expectDataAfterPackFooter; - } - - /** - * Whether there is additional data in InputStream after pack. - * - * @param e - * {@code true} if there is additional data in InputStream after - * pack. - */ - public void setExpectDataAfterPackFooter(boolean e) { - expectDataAfterPackFooter = e; - } - - /** - * Whether this instance will verify received objects are formatted - * correctly. - * - * @return {@code true} if this instance will verify received objects are - * formatted correctly. Validating objects requires more CPU time on - * this side of the connection. - */ - public boolean isCheckReceivedObjects() { - return objectChecker != null; - } - - /** - * Whether to enable checking received objects - * - * @param check - * {@code true} to enable checking received objects; false to - * assume all received objects are valid. - * @see #setObjectChecker(ObjectChecker) - */ - public void setCheckReceivedObjects(boolean check) { - if (check && objectChecker == null) - setObjectChecker(new ObjectChecker()); - else if (!check && objectChecker != null) - setObjectChecker(null); - } - - /** - * Set the object checking instance to verify each received object with - * - * @param impl - * if non-null the object checking instance to verify each - * received object with; null to disable object checking. - * @since 3.4 - */ - public void setObjectChecker(ObjectChecker impl) { - objectChecker = impl; - } - - /** - * Whether the client can request refs to be created. - * - * @return {@code true} if the client can request refs to be created. - */ - public boolean isAllowCreates() { - return allowCreates; - } - - /** - * Whether to permit create ref commands to be processed. - * - * @param canCreate - * {@code true} to permit create ref commands to be processed. - */ - public void setAllowCreates(boolean canCreate) { - allowCreates = canCreate; - } - - /** - * Whether the client can request refs to be deleted. - * - * @return {@code true} if the client can request refs to be deleted. - */ - public boolean isAllowDeletes() { - return allowAnyDeletes; - } - - /** - * Whether to permit delete ref commands to be processed. - * - * @param canDelete - * {@code true} to permit delete ref commands to be processed. - */ - public void setAllowDeletes(boolean canDelete) { - allowAnyDeletes = canDelete; - } - - /** - * Whether the client can delete from {@code refs/heads/}. - * - * @return {@code true} if the client can delete from {@code refs/heads/}. - * @since 3.6 - */ - public boolean isAllowBranchDeletes() { - return allowBranchDeletes; - } - - /** - * Configure whether to permit deletion of branches from the - * {@code refs/heads/} namespace. - * - * @param canDelete - * {@code true} to permit deletion of branches from the - * {@code refs/heads/} namespace. - * @since 3.6 - */ - public void setAllowBranchDeletes(boolean canDelete) { - allowBranchDeletes = canDelete; - } - - /** - * Whether the client can request non-fast-forward updates of a ref, - * possibly making objects unreachable. - * - * @return {@code true} if the client can request non-fast-forward updates - * of a ref, possibly making objects unreachable. - */ - public boolean isAllowNonFastForwards() { - return allowNonFastForwards; - } - - /** - * Configure whether to permit the client to ask for non-fast-forward - * updates of an existing ref. - * - * @param canRewind - * {@code true} to permit the client to ask for non-fast-forward - * updates of an existing ref. - */ - public void setAllowNonFastForwards(boolean canRewind) { - allowNonFastForwards = canRewind; - } - - /** - * Whether the client's commands should be performed as a single atomic - * transaction. - * - * @return {@code true} if the client's commands should be performed as a - * single atomic transaction. - * @since 4.4 - */ - public boolean isAtomic() { - return atomic; - } - - /** - * Configure whether to perform the client's commands as a single atomic - * transaction. - * - * @param atomic - * {@code true} to perform the client's commands as a single - * atomic transaction. - * @since 4.4 - */ - public void setAtomic(boolean atomic) { - this.atomic = atomic; - } - - /** - * Get identity of the user making the changes in the reflog. - * - * @return identity of the user making the changes in the reflog. - */ - public PersonIdent getRefLogIdent() { - return refLogIdent; - } - - /** - * Set the identity of the user appearing in the affected reflogs. - * <p> - * The timestamp portion of the identity is ignored. A new identity with the - * current timestamp will be created automatically when the updates occur - * and the log records are written. - * - * @param pi - * identity of the user. If null the identity will be - * automatically determined based on the repository - * configuration. - */ - public void setRefLogIdent(PersonIdent pi) { - refLogIdent = pi; - } - - /** - * Get the hook used while advertising the refs to the client - * - * @return the hook used while advertising the refs to the client - */ - public AdvertiseRefsHook getAdvertiseRefsHook() { - return advertiseRefsHook; - } - - /** - * Get the filter used while advertising the refs to the client - * - * @return the filter used while advertising the refs to the client - */ - public RefFilter getRefFilter() { - return refFilter; - } - - /** - * Set the hook used while advertising the refs to the client. - * <p> - * If the {@link org.eclipse.jgit.transport.AdvertiseRefsHook} chooses to - * call {@link #setAdvertisedRefs(Map,Set)}, only refs set by this hook - * <em>and</em> selected by the {@link org.eclipse.jgit.transport.RefFilter} - * will be shown to the client. Clients may still attempt to create or - * update a reference not advertised by the configured - * {@link org.eclipse.jgit.transport.AdvertiseRefsHook}. These attempts - * should be rejected by a matching - * {@link org.eclipse.jgit.transport.PreReceiveHook}. - * - * @param advertiseRefsHook - * the hook; may be null to show all refs. - */ - public void setAdvertiseRefsHook(AdvertiseRefsHook advertiseRefsHook) { - if (advertiseRefsHook != null) - this.advertiseRefsHook = advertiseRefsHook; - else - this.advertiseRefsHook = AdvertiseRefsHook.DEFAULT; - } - - /** - * Set the filter used while advertising the refs to the client. - * <p> - * Only refs allowed by this filter will be shown to the client. The filter - * is run against the refs specified by the - * {@link org.eclipse.jgit.transport.AdvertiseRefsHook} (if applicable). - * - * @param refFilter - * the filter; may be null to show all refs. - */ - public void setRefFilter(RefFilter refFilter) { - this.refFilter = refFilter != null ? refFilter : RefFilter.DEFAULT; - } - - /** - * Get timeout (in seconds) before aborting an IO operation. - * - * @return timeout (in seconds) before aborting an IO operation. - */ - public int getTimeout() { - return timeout; - } - - /** - * Set the timeout before willing to abort an IO call. - * - * @param seconds - * number of seconds to wait (with no data transfer occurring) - * before aborting an IO read or write operation with the - * connected client. - */ - public void setTimeout(int seconds) { - timeout = seconds; - } - - /** - * Set the maximum number of command bytes to read from the client. - * - * @param limit - * command limit in bytes; if 0 there is no limit. - * @since 4.7 - */ - public void setMaxCommandBytes(long limit) { - maxCommandBytes = limit; - } - - /** - * Set the maximum number of command bytes to discard from the client. - * <p> - * Discarding remaining bytes allows this instance to consume the rest of - * the command block and send a human readable over-limit error via the - * side-band channel. If the client sends an excessive number of bytes this - * limit kicks in and the instance disconnects, resulting in a non-specific - * 'pipe closed', 'end of stream', or similar generic error at the client. - * <p> - * When the limit is set to {@code -1} the implementation will default to - * the larger of {@code 3 * maxCommandBytes} or {@code 3 MiB}. - * - * @param limit - * discard limit in bytes; if 0 there is no limit; if -1 the - * implementation tries to set a reasonable default. - * @since 4.7 - */ - public void setMaxCommandDiscardBytes(long limit) { - maxDiscardBytes = limit; - } - - /** - * Set the maximum allowed Git object size. - * <p> - * If an object is larger than the given size the pack-parsing will throw an - * exception aborting the receive-pack operation. - * - * @param limit - * the Git object size limit. If zero then there is not limit. - */ - public void setMaxObjectSizeLimit(long limit) { - maxObjectSizeLimit = limit; - } - - /** - * Set the maximum allowed pack size. - * <p> - * A pack exceeding this size will be rejected. - * - * @param limit - * the pack size limit, in bytes - * @since 3.3 - */ - public void setMaxPackSizeLimit(long limit) { - if (limit < 0) - throw new IllegalArgumentException(MessageFormat.format( - JGitText.get().receivePackInvalidLimit, Long.valueOf(limit))); - maxPackSizeLimit = limit; - } - - /** - * Check whether the client expects a side-band stream. - * - * @return true if the client has advertised a side-band capability, false - * otherwise. - * @throws org.eclipse.jgit.transport.RequestNotYetReadException - * if the client's request has not yet been read from the wire, so - * we do not know if they expect side-band. Note that the client - * may have already written the request, it just has not been - * read. - */ - public boolean isSideBand() throws RequestNotYetReadException { - checkRequestWasRead(); - return enabledCapabilities.contains(CAPABILITY_SIDE_BAND_64K); - } - - /** - * Whether clients may request avoiding noisy progress messages. - * - * @return true if clients may request avoiding noisy progress messages. - * @since 4.0 - */ - public boolean isAllowQuiet() { - return allowQuiet; - } - - /** - * Configure if clients may request the server skip noisy messages. - * - * @param allow - * true to allow clients to request quiet behavior; false to - * refuse quiet behavior and send messages anyway. This may be - * necessary if processing is slow and the client-server network - * connection can timeout. - * @since 4.0 - */ - public void setAllowQuiet(boolean allow) { - allowQuiet = allow; - } - - /** - * Whether the server supports receiving push options. - * - * @return true if the server supports receiving push options. - * @since 4.5 - */ - public boolean isAllowPushOptions() { - return allowPushOptions; - } - - /** - * Configure if the server supports receiving push options. - * - * @param allow - * true to optionally accept option strings from the client. - * @since 4.5 - */ - public void setAllowPushOptions(boolean allow) { - allowPushOptions = allow; - } - - /** - * True if the client wants less verbose output. - * - * @return true if the client has requested the server to be less verbose. - * @throws org.eclipse.jgit.transport.RequestNotYetReadException - * if the client's request has not yet been read from the wire, - * so we do not know if they expect side-band. Note that the - * client may have already written the request, it just has not - * been read. - * @since 4.0 - */ - public boolean isQuiet() throws RequestNotYetReadException { - checkRequestWasRead(); - return quiet; - } - - /** - * Set the configuration for push certificate verification. - * - * @param cfg - * new configuration; if this object is null or its {@link - * SignedPushConfig#getCertNonceSeed()} is null, push certificate - * verification will be disabled. - * @since 4.1 - */ - public void setSignedPushConfig(SignedPushConfig cfg) { - signedPushConfig = cfg; - } - - private PushCertificateParser getPushCertificateParser() { - if (pushCertificateParser == null) { - pushCertificateParser = new PushCertificateParser(db, signedPushConfig); - } - return pushCertificateParser; - } - - /** - * Get the user agent of the client. - * <p> - * If the client is new enough to use {@code agent=} capability that value - * will be returned. Older HTTP clients may also supply their version using - * the HTTP {@code User-Agent} header. The capability overrides the HTTP - * header if both are available. - * <p> - * When an HTTP request has been received this method returns the HTTP - * {@code User-Agent} header value until capabilities have been parsed. - * - * @return user agent supplied by the client. Available only if the client - * is new enough to advertise its user agent. - * @since 4.0 - */ - public String getPeerUserAgent() { - return UserAgent.getAgent(enabledCapabilities, userAgent); - } - - /** - * Get all of the command received by the current request. - * - * @return all of the command received by the current request. - */ - public List<ReceiveCommand> getAllCommands() { - return Collections.unmodifiableList(commands); - } - - /** - * Send an error message to the client. - * <p> - * If any error messages are sent before the references are advertised to - * the client, the errors will be sent instead of the advertisement and the - * receive operation will be aborted. All clients should receive and display - * such early stage errors. - * <p> - * If the reference advertisements have already been sent, messages are sent - * in a side channel. If the client doesn't support receiving messages, the - * message will be discarded, with no other indication to the caller or to - * the client. - * <p> - * {@link org.eclipse.jgit.transport.PreReceiveHook}s should always try to - * use - * {@link org.eclipse.jgit.transport.ReceiveCommand#setResult(Result, String)} - * with a result status of - * {@link org.eclipse.jgit.transport.ReceiveCommand.Result#REJECTED_OTHER_REASON} - * to indicate any reasons for rejecting an update. Messages attached to a - * command are much more likely to be returned to the client. - * - * @param what - * string describing the problem identified by the hook. The - * string must not end with an LF, and must not contain an LF. - */ - public void sendError(String what) { - if (refs == null) { - if (advertiseError == null) - advertiseError = new StringBuilder(); - advertiseError.append(what).append('\n'); - } else { - msgOutWrapper.write(Constants.encode("error: " + what + "\n")); //$NON-NLS-1$ //$NON-NLS-2$ - } - } - - private void fatalError(String msg) { - if (errOut != null) { - try { - errOut.write(Constants.encode(msg)); - errOut.flush(); - } catch (IOException e) { - // Ignore write failures - } - } else { - sendError(msg); - } - } - - /** - * Send a message to the client, if it supports receiving them. - * <p> - * If the client doesn't support receiving messages, the message will be - * discarded, with no other indication to the caller or to the client. - * - * @param what - * string describing the problem identified by the hook. The - * string must not end with an LF, and must not contain an LF. - */ - public void sendMessage(String what) { - msgOutWrapper.write(Constants.encode(what + "\n")); //$NON-NLS-1$ - } - - /** - * Get an underlying stream for sending messages to the client. - * - * @return an underlying stream for sending messages to the client. - */ - public OutputStream getMessageOutputStream() { - return msgOutWrapper; - } - - /** - * Get the size of the received pack file including the index size. - * - * This can only be called if the pack is already received. - * - * @return the size of the received pack including index size - * @throws java.lang.IllegalStateException - * if called before the pack has been received - * @since 3.3 - */ - public long getPackSize() { - if (packSize != null) - return packSize.longValue(); - throw new IllegalStateException(JGitText.get().packSizeNotSetYet); - } - - /** - * Get the commits from the client's shallow file. - * - * @return if the client is a shallow repository, the list of edge commits - * that define the client's shallow boundary. Empty set if the client - * is earlier than Git 1.9, or is a full clone. - * @since 3.5 - */ - protected Set<ObjectId> getClientShallowCommits() { - return clientShallowCommits; - } - - /** - * Whether any commands to be executed have been read. - * - * @return {@code true} if any commands to be executed have been read. - */ - protected boolean hasCommands() { - return !commands.isEmpty(); - } - - /** - * Whether an error occurred that should be advertised. - * - * @return true if an error occurred that should be advertised. - */ - protected boolean hasError() { - return advertiseError != null; - } - - /** - * Initialize the instance with the given streams. - * - * @param input - * raw input to read client commands and pack data from. Caller - * must ensure the input is buffered, otherwise read performance - * may suffer. - * @param output - * response back to the Git network client. Caller must ensure - * the output is buffered, otherwise write performance may - * suffer. - * @param messages - * secondary "notice" channel to send additional messages out - * through. When run over SSH this should be tied back to the - * standard error channel of the command execution. For most - * other network connections this should be null. - */ - protected void init(final InputStream input, final OutputStream output, - final OutputStream messages) { - origOut = output; - rawIn = input; - rawOut = output; - msgOut = messages; - - if (timeout > 0) { - final Thread caller = Thread.currentThread(); - timer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$ - timeoutIn = new TimeoutInputStream(rawIn, timer); - TimeoutOutputStream o = new TimeoutOutputStream(rawOut, timer); - timeoutIn.setTimeout(timeout * 1000); - o.setTimeout(timeout * 1000); - rawIn = timeoutIn; - rawOut = o; - } - - pckIn = new PacketLineIn(rawIn); - pckOut = new PacketLineOut(rawOut); - pckOut.setFlushOnEnd(false); - - enabledCapabilities = new HashSet<>(); - commands = new ArrayList<>(); - } - - /** - * Get advertised refs, or the default if not explicitly advertised. - * - * @return advertised refs, or the default if not explicitly advertised. - */ - protected Map<String, Ref> getAdvertisedOrDefaultRefs() { - if (refs == null) - setAdvertisedRefs(null, null); - return refs; - } - - /** - * Receive a pack from the stream and check connectivity if necessary. - * - * @throws java.io.IOException - * an error occurred during unpacking or connectivity checking. - */ - protected void receivePackAndCheckConnectivity() throws IOException { - receivePack(); - if (needCheckConnectivity()) { - checkSubmodules(); - checkConnectivity(); - } - parser = null; - } - - /** - * Unlock the pack written by this object. - * - * @throws java.io.IOException - * the pack could not be unlocked. - */ - protected void unlockPack() throws IOException { - if (packLock != null) { - packLock.unlock(); - packLock = null; - } - } - - /** - * Generate an advertisement of available refs and capabilities. - * - * @param adv - * the advertisement formatter. - * @throws java.io.IOException - * the formatter failed to write an advertisement. - * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException - * the hook denied advertisement. - */ - public void sendAdvertisedRefs(RefAdvertiser adv) - throws IOException, ServiceMayNotContinueException { - if (advertiseError != null) { - adv.writeOne("ERR " + advertiseError); //$NON-NLS-1$ - return; - } - - try { - advertiseRefsHook.advertiseRefs(this); - } catch (ServiceMayNotContinueException fail) { - if (fail.getMessage() != null) { - adv.writeOne("ERR " + fail.getMessage()); //$NON-NLS-1$ - fail.setOutput(); - } - throw fail; - } - - adv.init(db); - adv.advertiseCapability(CAPABILITY_SIDE_BAND_64K); - adv.advertiseCapability(CAPABILITY_DELETE_REFS); - adv.advertiseCapability(CAPABILITY_REPORT_STATUS); - if (allowQuiet) - adv.advertiseCapability(CAPABILITY_QUIET); - String nonce = getPushCertificateParser().getAdvertiseNonce(); - if (nonce != null) { - adv.advertiseCapability(nonce); - } - if (db.getRefDatabase().performsAtomicTransactions()) - adv.advertiseCapability(CAPABILITY_ATOMIC); - if (allowOfsDelta) - adv.advertiseCapability(CAPABILITY_OFS_DELTA); - if (allowPushOptions) { - adv.advertiseCapability(CAPABILITY_PUSH_OPTIONS); - } - adv.advertiseCapability(OPTION_AGENT, UserAgent.get()); - adv.send(getAdvertisedOrDefaultRefs().values()); - for (ObjectId obj : advertisedHaves) - adv.advertiseHave(obj); - if (adv.isEmpty()) - adv.advertiseId(ObjectId.zeroId(), "capabilities^{}"); //$NON-NLS-1$ - adv.end(); - } - - /** - * Returns the statistics on the received pack if available. This should be - * called after {@link #receivePack} is called. - * - * @return ReceivedPackStatistics - * @since 4.6 - */ - @Nullable - public ReceivedPackStatistics getReceivedPackStatistics() { - return stats; - } - - /** - * Receive a list of commands from the input. - * - * @throws java.io.IOException - */ - protected void recvCommands() throws IOException { - PacketLineIn pck = maxCommandBytes > 0 - ? new PacketLineIn(rawIn, maxCommandBytes) - : pckIn; - PushCertificateParser certParser = getPushCertificateParser(); - boolean firstPkt = true; - try { - for (;;) { - String line; - try { - line = pck.readString(); - } catch (EOFException eof) { - if (commands.isEmpty()) - return; - throw eof; - } - if (PacketLineIn.isEnd(line)) { - break; - } - - if (line.length() >= 48 && line.startsWith("shallow ")) { //$NON-NLS-1$ - parseShallow(line.substring(8, 48)); - continue; - } - - if (firstPkt) { - firstPkt = false; - FirstCommand firstLine = FirstCommand.fromLine(line); - enabledCapabilities = firstLine.getCapabilities(); - line = firstLine.getLine(); - enableCapabilities(); - - if (line.equals(GitProtocolConstants.OPTION_PUSH_CERT)) { - certParser.receiveHeader(pck, !isBiDirectionalPipe()); - continue; - } - } - - if (line.equals(PushCertificateParser.BEGIN_SIGNATURE)) { - certParser.receiveSignature(pck); - continue; - } - - ReceiveCommand cmd = parseCommand(line); - if (cmd.getRefName().equals(Constants.HEAD)) { - cmd.setResult(Result.REJECTED_CURRENT_BRANCH); - } else { - cmd.setRef(refs.get(cmd.getRefName())); - } - commands.add(cmd); - if (certParser.enabled()) { - certParser.addCommand(cmd); - } - } - pushCert = certParser.build(); - if (hasCommands()) { - readPostCommands(pck); - } - } catch (PackProtocolException e) { - discardCommands(); - fatalError(e.getMessage()); - throw e; - } catch (InputOverLimitIOException e) { - String msg = JGitText.get().tooManyCommands; - discardCommands(); - fatalError(msg); - throw new PackProtocolException(msg); - } - } - - private void discardCommands() { - if (sideBand) { - long max = maxDiscardBytes; - if (max < 0) { - max = Math.max(3 * maxCommandBytes, 3L << 20); - } - try { - new PacketLineIn(rawIn, max).discardUntilEnd(); - } catch (IOException e) { - // Ignore read failures attempting to discard. - } - } - } - - private void parseShallow(String idStr) throws PackProtocolException { - ObjectId id; - try { - id = ObjectId.fromString(idStr); - } catch (InvalidObjectIdException e) { - throw new PackProtocolException(e.getMessage(), e); - } - clientShallowCommits.add(id); - } - - static ReceiveCommand parseCommand(String line) throws PackProtocolException { - if (line == null || line.length() < 83) { - throw new PackProtocolException( - JGitText.get().errorInvalidProtocolWantedOldNewRef); - } - String oldStr = line.substring(0, 40); - String newStr = line.substring(41, 81); - ObjectId oldId, newId; - try { - oldId = ObjectId.fromString(oldStr); - newId = ObjectId.fromString(newStr); - } catch (InvalidObjectIdException e) { - throw new PackProtocolException( - JGitText.get().errorInvalidProtocolWantedOldNewRef, e); - } - String name = line.substring(82); - if (!Repository.isValidRefName(name)) { - throw new PackProtocolException( - JGitText.get().errorInvalidProtocolWantedOldNewRef); - } - return new ReceiveCommand(oldId, newId, name); - } - - /** - * @param in - * request stream. - * @throws IOException - * request line cannot be read. - */ - void readPostCommands(PacketLineIn in) throws IOException { - // Do nothing by default. - } - - /** - * Enable capabilities based on a previously read capabilities line. - */ - protected void enableCapabilities() { - sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K); - quiet = allowQuiet && isCapabilityEnabled(CAPABILITY_QUIET); - if (sideBand) { - OutputStream out = rawOut; - - rawOut = new SideBandOutputStream(CH_DATA, MAX_BUF, out); - msgOut = new SideBandOutputStream(CH_PROGRESS, MAX_BUF, out); - errOut = new SideBandOutputStream(CH_ERROR, MAX_BUF, out); - - pckOut = new PacketLineOut(rawOut); - pckOut.setFlushOnEnd(false); - } - } - - /** - * Check if the peer requested a capability. - * - * @param name - * protocol name identifying the capability. - * @return true if the peer requested the capability to be enabled. - */ - protected boolean isCapabilityEnabled(String name) { - return enabledCapabilities.contains(name); - } - - void checkRequestWasRead() { - if (enabledCapabilities == null) - throw new RequestNotYetReadException(); - } - - /** - * Whether a pack is expected based on the list of commands. - * - * @return {@code true} if a pack is expected based on the list of commands. - */ - protected boolean needPack() { - for (ReceiveCommand cmd : commands) { - if (cmd.getType() != ReceiveCommand.Type.DELETE) - return true; - } - return false; - } - - /** - * Receive a pack from the input and store it in the repository. - * - * @throws IOException - * an error occurred reading or indexing the pack. - */ - private void receivePack() throws IOException { - // It might take the client a while to pack the objects it needs - // to send to us. We should increase our timeout so we don't - // abort while the client is computing. - // - if (timeoutIn != null) - timeoutIn.setTimeout(10 * timeout * 1000); - - ProgressMonitor receiving = NullProgressMonitor.INSTANCE; - ProgressMonitor resolving = NullProgressMonitor.INSTANCE; - if (sideBand && !quiet) - resolving = new SideBandProgressMonitor(msgOut); - - try (ObjectInserter ins = db.newObjectInserter()) { - String lockMsg = "jgit receive-pack"; //$NON-NLS-1$ - if (getRefLogIdent() != null) - lockMsg += " from " + getRefLogIdent().toExternalString(); //$NON-NLS-1$ - - parser = ins.newPackParser(packInputStream()); - parser.setAllowThin(true); - parser.setNeedNewObjectIds(checkReferencedIsReachable); - parser.setNeedBaseObjectIds(checkReferencedIsReachable); - parser.setCheckEofAfterPackFooter(!biDirectionalPipe - && !isExpectDataAfterPackFooter()); - parser.setExpectDataAfterPackFooter(isExpectDataAfterPackFooter()); - parser.setObjectChecker(objectChecker); - parser.setLockMessage(lockMsg); - parser.setMaxObjectSizeLimit(maxObjectSizeLimit); - packLock = parser.parse(receiving, resolving); - packSize = Long.valueOf(parser.getPackSize()); - stats = parser.getReceivedPackStatistics(); - ins.flush(); - } - - if (timeoutIn != null) - timeoutIn.setTimeout(timeout * 1000); - } - - private InputStream packInputStream() { - InputStream packIn = rawIn; - if (maxPackSizeLimit >= 0) { - packIn = new LimitedInputStream(packIn, maxPackSizeLimit) { - @Override - protected void limitExceeded() throws TooLargePackException { - throw new TooLargePackException(limit); - } - }; - } - return packIn; - } - - private boolean needCheckConnectivity() { - return isCheckReceivedObjects() - || isCheckReferencedObjectsAreReachable() - || !getClientShallowCommits().isEmpty(); - } - - private void checkSubmodules() - throws IOException { - ObjectDatabase odb = db.getObjectDatabase(); - if (objectChecker == null) { - return; - } - for (GitmoduleEntry entry : objectChecker.getGitsubmodules()) { - AnyObjectId blobId = entry.getBlobId(); - ObjectLoader blob = odb.open(blobId, Constants.OBJ_BLOB); - - try { - SubmoduleValidator.assertValidGitModulesFile( - new String(blob.getBytes(), UTF_8)); - } catch (LargeObjectException | SubmoduleValidationException e) { - throw new IOException(e); - } - } - } - - private void checkConnectivity() throws IOException { - ObjectIdSubclassMap<ObjectId> baseObjects = null; - ObjectIdSubclassMap<ObjectId> providedObjects = null; - ProgressMonitor checking = NullProgressMonitor.INSTANCE; - if (sideBand && !quiet) { - SideBandProgressMonitor m = new SideBandProgressMonitor(msgOut); - m.setDelayStart(750, TimeUnit.MILLISECONDS); - checking = m; - } - - if (checkReferencedIsReachable) { - baseObjects = parser.getBaseObjectIds(); - providedObjects = parser.getNewObjectIds(); - } - parser = null; - - try (ObjectWalk ow = new ObjectWalk(db)) { - if (baseObjects != null) { - ow.sort(RevSort.TOPO); - if (!baseObjects.isEmpty()) - ow.sort(RevSort.BOUNDARY, true); - } - - for (ReceiveCommand cmd : commands) { - if (cmd.getResult() != Result.NOT_ATTEMPTED) - continue; - if (cmd.getType() == ReceiveCommand.Type.DELETE) - continue; - ow.markStart(ow.parseAny(cmd.getNewId())); - } - for (ObjectId have : advertisedHaves) { - RevObject o = ow.parseAny(have); - ow.markUninteresting(o); - - if (baseObjects != null && !baseObjects.isEmpty()) { - o = ow.peel(o); - if (o instanceof RevCommit) - o = ((RevCommit) o).getTree(); - if (o instanceof RevTree) - ow.markUninteresting(o); - } - } - - checking.beginTask(JGitText.get().countingObjects, - ProgressMonitor.UNKNOWN); - RevCommit c; - while ((c = ow.next()) != null) { - checking.update(1); - if (providedObjects != null // - && !c.has(RevFlag.UNINTERESTING) // - && !providedObjects.contains(c)) - throw new MissingObjectException(c, Constants.TYPE_COMMIT); - } - - RevObject o; - while ((o = ow.nextObject()) != null) { - checking.update(1); - if (o.has(RevFlag.UNINTERESTING)) - continue; - - if (providedObjects != null) { - if (providedObjects.contains(o)) - continue; - else - throw new MissingObjectException(o, o.getType()); - } - - if (o instanceof RevBlob && !db.getObjectDatabase().has(o)) - throw new MissingObjectException(o, Constants.TYPE_BLOB); - } - checking.endTask(); - - if (baseObjects != null) { - for (ObjectId id : baseObjects) { - o = ow.parseAny(id); - if (!o.has(RevFlag.UNINTERESTING)) - throw new MissingObjectException(o, o.getType()); - } - } - } - } - - /** - * Validate the command list. - */ - protected void validateCommands() { - for (ReceiveCommand cmd : commands) { - final Ref ref = cmd.getRef(); - if (cmd.getResult() != Result.NOT_ATTEMPTED) - continue; - - if (cmd.getType() == ReceiveCommand.Type.DELETE) { - if (!isAllowDeletes()) { - // Deletes are not supported on this repository. - cmd.setResult(Result.REJECTED_NODELETE); - continue; - } - if (!isAllowBranchDeletes() - && ref.getName().startsWith(Constants.R_HEADS)) { - // Branches cannot be deleted, but other refs can. - cmd.setResult(Result.REJECTED_NODELETE); - continue; - } - } - - if (cmd.getType() == ReceiveCommand.Type.CREATE) { - if (!isAllowCreates()) { - cmd.setResult(Result.REJECTED_NOCREATE); - continue; - } - - if (ref != null && !isAllowNonFastForwards()) { - // Creation over an existing ref is certainly not going - // to be a fast-forward update. We can reject it early. - // - cmd.setResult(Result.REJECTED_NONFASTFORWARD); - continue; - } - - if (ref != null) { - // A well behaved client shouldn't have sent us a - // create command for a ref we advertised to it. - // - cmd.setResult(Result.REJECTED_OTHER_REASON, - JGitText.get().refAlreadyExists); - continue; - } - } - - if (cmd.getType() == ReceiveCommand.Type.DELETE && ref != null) { - ObjectId id = ref.getObjectId(); - if (id == null) { - id = ObjectId.zeroId(); - } - if (!ObjectId.zeroId().equals(cmd.getOldId()) - && !id.equals(cmd.getOldId())) { - // Delete commands can be sent with the old id matching our - // advertised value, *OR* with the old id being 0{40}. Any - // other requested old id is invalid. - // - cmd.setResult(Result.REJECTED_OTHER_REASON, - JGitText.get().invalidOldIdSent); - continue; - } - } - - if (cmd.getType() == ReceiveCommand.Type.UPDATE) { - if (ref == null) { - // The ref must have been advertised in order to be updated. - // - cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().noSuchRef); - continue; - } - ObjectId id = ref.getObjectId(); - if (id == null) { - // We cannot update unborn branch - cmd.setResult(Result.REJECTED_OTHER_REASON, - JGitText.get().cannotUpdateUnbornBranch); - continue; - } - - if (!id.equals(cmd.getOldId())) { - // A properly functioning client will send the same - // object id we advertised. - // - cmd.setResult(Result.REJECTED_OTHER_REASON, - JGitText.get().invalidOldIdSent); - continue; - } - - // Is this possibly a non-fast-forward style update? - // - RevObject oldObj, newObj; - try { - oldObj = walk.parseAny(cmd.getOldId()); - } catch (IOException e) { - cmd.setResult(Result.REJECTED_MISSING_OBJECT, cmd - .getOldId().name()); - continue; - } - - try { - newObj = walk.parseAny(cmd.getNewId()); - } catch (IOException e) { - cmd.setResult(Result.REJECTED_MISSING_OBJECT, cmd - .getNewId().name()); - continue; - } - - if (oldObj instanceof RevCommit && newObj instanceof RevCommit) { - try { - if (walk.isMergedInto((RevCommit) oldObj, - (RevCommit) newObj)) - cmd.setTypeFastForwardUpdate(); - else - cmd.setType(ReceiveCommand.Type.UPDATE_NONFASTFORWARD); - } catch (MissingObjectException e) { - cmd.setResult(Result.REJECTED_MISSING_OBJECT, e - .getMessage()); - } catch (IOException e) { - cmd.setResult(Result.REJECTED_OTHER_REASON); - } - } else { - cmd.setType(ReceiveCommand.Type.UPDATE_NONFASTFORWARD); - } - - if (cmd.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD - && !isAllowNonFastForwards()) { - cmd.setResult(Result.REJECTED_NONFASTFORWARD); - continue; - } - } - - if (!cmd.getRefName().startsWith(Constants.R_REFS) - || !Repository.isValidRefName(cmd.getRefName())) { - cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().funnyRefname); - } - } - } - - /** - * Whether any commands have been rejected so far. - * - * @return if any commands have been rejected so far. - * @since 3.6 - */ - protected boolean anyRejects() { - for (ReceiveCommand cmd : commands) { - if (cmd.getResult() != Result.NOT_ATTEMPTED && cmd.getResult() != Result.OK) - return true; - } - return false; - } - - /** - * Set the result to fail for any command that was not processed yet. - * - * @since 3.6 - */ - protected void failPendingCommands() { - ReceiveCommand.abort(commands); - } - - /** - * Filter the list of commands according to result. - * - * @param want - * desired status to filter by. - * @return a copy of the command list containing only those commands with the - * desired status. - */ - protected List<ReceiveCommand> filterCommands(Result want) { - return ReceiveCommand.filter(commands, want); - } - - /** - * Execute commands to update references. - */ - protected void executeCommands() { - List<ReceiveCommand> toApply = filterCommands(Result.NOT_ATTEMPTED); - if (toApply.isEmpty()) - return; - - ProgressMonitor updating = NullProgressMonitor.INSTANCE; - if (sideBand) { - SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut); - pm.setDelayStart(250, TimeUnit.MILLISECONDS); - updating = pm; - } - - BatchRefUpdate batch = db.getRefDatabase().newBatchUpdate(); - batch.setAllowNonFastForwards(isAllowNonFastForwards()); - batch.setAtomic(isAtomic()); - batch.setRefLogIdent(getRefLogIdent()); - batch.setRefLogMessage("push", true); //$NON-NLS-1$ - batch.addCommand(toApply); - try { - batch.setPushCertificate(getPushCertificate()); - batch.execute(walk, updating); - } catch (IOException err) { - for (ReceiveCommand cmd : toApply) { - if (cmd.getResult() == Result.NOT_ATTEMPTED) - cmd.reject(err); - } - } - } - - /** - * Send a status report. - * - * @param forClient - * true if this report is for a Git client, false if it is for an - * end-user. - * @param unpackError - * an error that occurred during unpacking, or {@code null} - * @param out - * the reporter for sending the status strings. - * @throws java.io.IOException - * an error occurred writing the status report. - */ - protected void sendStatusReport(final boolean forClient, - final Throwable unpackError, final Reporter out) throws IOException { - if (unpackError != null) { - out.sendString("unpack error " + unpackError.getMessage()); //$NON-NLS-1$ - if (forClient) { - for (ReceiveCommand cmd : commands) { - out.sendString("ng " + cmd.getRefName() //$NON-NLS-1$ - + " n/a (unpacker error)"); //$NON-NLS-1$ - } - } - return; - } - - if (forClient) - out.sendString("unpack ok"); //$NON-NLS-1$ - for (ReceiveCommand cmd : commands) { - if (cmd.getResult() == Result.OK) { - if (forClient) - out.sendString("ok " + cmd.getRefName()); //$NON-NLS-1$ - continue; - } - - final StringBuilder r = new StringBuilder(); - if (forClient) - r.append("ng ").append(cmd.getRefName()).append(" "); //$NON-NLS-1$ //$NON-NLS-2$ - else - r.append(" ! [rejected] ").append(cmd.getRefName()).append(" ("); //$NON-NLS-1$ //$NON-NLS-2$ - - switch (cmd.getResult()) { - case NOT_ATTEMPTED: - r.append("server bug; ref not processed"); //$NON-NLS-1$ - break; - - case REJECTED_NOCREATE: - r.append("creation prohibited"); //$NON-NLS-1$ - break; - - case REJECTED_NODELETE: - r.append("deletion prohibited"); //$NON-NLS-1$ - break; - - case REJECTED_NONFASTFORWARD: - r.append("non-fast forward"); //$NON-NLS-1$ - break; - - case REJECTED_CURRENT_BRANCH: - r.append("branch is currently checked out"); //$NON-NLS-1$ - break; - - case REJECTED_MISSING_OBJECT: - if (cmd.getMessage() == null) - r.append("missing object(s)"); //$NON-NLS-1$ - else if (cmd.getMessage().length() == Constants.OBJECT_ID_STRING_LENGTH) { - r.append("object "); //$NON-NLS-1$ - r.append(cmd.getMessage()); - r.append(" missing"); //$NON-NLS-1$ - } else - r.append(cmd.getMessage()); - break; - - case REJECTED_OTHER_REASON: - if (cmd.getMessage() == null) - r.append("unspecified reason"); //$NON-NLS-1$ - else - r.append(cmd.getMessage()); - break; - - case LOCK_FAILURE: - r.append("failed to lock"); //$NON-NLS-1$ - break; - - case OK: - // We shouldn't have reached this case (see 'ok' case above). - continue; - } - if (!forClient) - r.append(")"); //$NON-NLS-1$ - out.sendString(r.toString()); - } - } - - /** - * Close and flush (if necessary) the underlying streams. - * - * @throws java.io.IOException - */ - protected void close() throws IOException { - if (sideBand) { - // If we are using side band, we need to send a final - // flush-pkt to tell the remote peer the side band is - // complete and it should stop decoding. We need to - // use the original output stream as rawOut is now the - // side band data channel. - // - ((SideBandOutputStream) msgOut).flushBuffer(); - ((SideBandOutputStream) rawOut).flushBuffer(); - - PacketLineOut plo = new PacketLineOut(origOut); - plo.setFlushOnEnd(false); - plo.end(); - } - - if (biDirectionalPipe) { - // If this was a native git connection, flush the pipe for - // the caller. For smart HTTP we don't do this flush and - // instead let the higher level HTTP servlet code do it. - // - if (!sideBand && msgOut != null) - msgOut.flush(); - rawOut.flush(); - } - } - - /** - * Release any resources used by this object. - * - * @throws java.io.IOException - * the pack could not be unlocked. - */ - protected void release() throws IOException { - walk.close(); - unlockPack(); - timeoutIn = null; - rawIn = null; - rawOut = null; - msgOut = null; - pckIn = null; - pckOut = null; - refs = null; - // Keep the capabilities. If responses are sent after this release - // we need to remember at least whether sideband communication has to be - // used - commands = null; - if (timer != null) { - try { - timer.terminate(); - } finally { - timer = null; - } - } - } - - /** Interface for reporting status messages. */ - static abstract class Reporter { - abstract void sendString(String s) throws IOException; - } -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProviderUserInfo.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProviderUserInfo.java index d901021788..6325a23530 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProviderUserInfo.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProviderUserInfo.java @@ -107,10 +107,9 @@ public class CredentialsProviderUserInfo implements UserInfo, if (provider.get(uri, v)) { passphrase = v.getValue(); return true; - } else { - passphrase = null; - return false; } + passphrase = null; + return false; } /** {@inheritDoc} */ @@ -120,10 +119,9 @@ public class CredentialsProviderUserInfo implements UserInfo, if (provider.get(uri, p)) { password = new String(p.getValue()); return true; - } else { - password = null; - return false; } + password = null; + return false; } private CredentialItem.StringType newPrompt(String msg) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java index e3c0bc629a..2fd074674d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java @@ -280,7 +280,7 @@ public final class GitProtocolConstants { */ public static final String COMMAND_FETCH = "fetch"; //$NON-NLS-1$ - static enum MultiAck { + enum MultiAck { OFF, CONTINUE, DETAILED; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java index 01f6fec7e4..72e95e6ba3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java @@ -135,9 +135,8 @@ public class HMACSHA1NonceGenerator implements NonceGenerator { if (nonceStampSlop <= slop) { return NonceStatus.OK; - } else { - return NonceStatus.SLOP; } + return NonceStatus.SLOP; } private static final String HEX = "0123456789ABCDEF"; //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java index 2b2795fefb..a9cd677667 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java @@ -103,7 +103,7 @@ public abstract class PackParser { private static final int BUFFER_SIZE = 8192; /** Location data is being obtained from. */ - public static enum Source { + public enum Source { /** Data is read from the incoming stream. */ INPUT, diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java index d73e1939a6..bbdd55583c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java @@ -95,7 +95,7 @@ public class PacketLineIn { @Deprecated public static final String DELIM = new StringBuilder(0).toString(); /* must not string pool */ - static enum AckNackResult { + enum AckNackResult { /** NAK */ NAK, /** ACK */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java index 14ccddfb61..c2f69e20be 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java @@ -215,7 +215,7 @@ final class ProtocolV2Parser { && line2.equals(OPTION_SIDEBAND_ALL)) { reqBuilder.setSidebandAll(true); } else if (line2.startsWith("packfile-uris ")) { //$NON-NLS-1$ - for (String s : line2.substring(14).split(",")) { + for (String s : line2.substring(14).split(",")) { //$NON-NLS-1$ reqBuilder.addPackfileUriProtocol(s); } } else { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java index 89c1a93788..3653879c7d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java @@ -43,7 +43,7 @@ package org.eclipse.jgit.transport; -import static org.eclipse.jgit.transport.BaseReceivePack.parseCommand; +import static org.eclipse.jgit.transport.ReceivePack.parseCommand; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CERT; import java.io.EOFException; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java index a9a995cd3f..d58bc6953b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java @@ -65,14 +65,14 @@ import org.eclipse.jgit.revwalk.RevWalk; /** * A command being processed by - * {@link org.eclipse.jgit.transport.BaseReceivePack}. + * {@link org.eclipse.jgit.transport.ReceivePack}. * <p> * This command instance roughly translates to the server side representation of * the {@link org.eclipse.jgit.transport.RemoteRefUpdate} created by the client. */ public class ReceiveCommand { /** Type of operation requested. */ - public static enum Type { + public enum Type { /** Create a new ref; the ref must not already exist. */ CREATE, @@ -98,7 +98,7 @@ public class ReceiveCommand { } /** Result of the update command. */ - public static enum Result { + public enum Result { /** The command has not yet been attempted by the server. */ NOT_ATTEMPTED, @@ -290,7 +290,7 @@ public class ReceiveCommand { /** * Create a new command for - * {@link org.eclipse.jgit.transport.BaseReceivePack}. + * {@link org.eclipse.jgit.transport.ReceivePack}. * * @param oldId * the expected old object id; must not be null. Use @@ -334,7 +334,7 @@ public class ReceiveCommand { /** * Create a new command for - * {@link org.eclipse.jgit.transport.BaseReceivePack}. + * {@link org.eclipse.jgit.transport.ReceivePack}. * * @param oldId * the old object id; must not be null. Use @@ -768,9 +768,9 @@ public class ReceiveCommand { * * @param rp * receive-pack session. - * @since 2.0 + * @since 5.6 */ - public void execute(BaseReceivePack rp) { + public void execute(ReceivePack rp) { try { String expTarget = getOldSymref(); boolean detach = getNewSymref() != null diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java index d6adf1e0d8..fab9890c7c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java @@ -43,36 +43,255 @@ package org.eclipse.jgit.transport; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.lib.Constants.HEAD; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_ATOMIC; +import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_DELETE_REFS; +import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_OFS_DELTA; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_OPTIONS; +import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_QUIET; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_REPORT_STATUS; +import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SIDE_BAND_64K; +import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; +import static org.eclipse.jgit.transport.SideBandOutputStream.CH_DATA; +import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR; +import static org.eclipse.jgit.transport.SideBandOutputStream.CH_PROGRESS; +import static org.eclipse.jgit.transport.SideBandOutputStream.MAX_BUF; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.errors.InvalidObjectIdException; +import org.eclipse.jgit.errors.LargeObjectException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.PackProtocolException; +import org.eclipse.jgit.errors.TooLargePackException; import org.eclipse.jgit.errors.UnpackException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.file.PackLock; +import org.eclipse.jgit.internal.submodule.SubmoduleValidator; +import org.eclipse.jgit.internal.submodule.SubmoduleValidator.SubmoduleValidationException; +import org.eclipse.jgit.internal.transport.parser.FirstCommand; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.GitmoduleEntry; import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectChecker; +import org.eclipse.jgit.lib.ObjectDatabase; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdSubclassMap; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.ObjectWalk; +import org.eclipse.jgit.revwalk.RevBlob; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.PacketLineIn.InputOverLimitIOException; import org.eclipse.jgit.transport.ReceiveCommand.Result; import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser; +import org.eclipse.jgit.util.io.InterruptTimer; +import org.eclipse.jgit.util.io.LimitedInputStream; +import org.eclipse.jgit.util.io.TimeoutInputStream; +import org.eclipse.jgit.util.io.TimeoutOutputStream; /** * Implements the server side of a push connection, receiving objects. */ -public class ReceivePack extends BaseReceivePack { +public class ReceivePack { + /** + * Data in the first line of a request, the line itself plus capabilities. + * + * @deprecated Use {@link FirstCommand} instead. + * @since 5.6 + */ + @Deprecated + public static class FirstLine { + private final FirstCommand command; + + /** + * Parse the first line of a receive-pack request. + * + * @param line + * line from the client. + */ + public FirstLine(String line) { + command = FirstCommand.fromLine(line); + } + + /** @return non-capabilities part of the line. */ + public String getLine() { + return command.getLine(); + } + + /** @return capabilities parsed from the line. */ + public Set<String> getCapabilities() { + return command.getCapabilities(); + } + } + + /** Database we write the stored objects into. */ + private final Repository db; + + /** Revision traversal support over {@link #db}. */ + private final RevWalk walk; + + /** + * Is the client connection a bi-directional socket or pipe? + * <p> + * If true, this class assumes it can perform multiple read and write cycles + * with the client over the input and output streams. This matches the + * functionality available with a standard TCP/IP connection, or a local + * operating system or in-memory pipe. + * <p> + * If false, this class runs in a read everything then output results mode, + * making it suitable for single round-trip systems RPCs such as HTTP. + */ + private boolean biDirectionalPipe = true; + + /** Expecting data after the pack footer */ + private boolean expectDataAfterPackFooter; + + /** Should an incoming transfer validate objects? */ + private ObjectChecker objectChecker; + + /** Should an incoming transfer permit create requests? */ + private boolean allowCreates; + + /** Should an incoming transfer permit delete requests? */ + private boolean allowAnyDeletes; + + private boolean allowBranchDeletes; + + /** Should an incoming transfer permit non-fast-forward requests? */ + private boolean allowNonFastForwards; + + /** Should an incoming transfer permit push options? **/ + private boolean allowPushOptions; + + /** + * Should the requested ref updates be performed as a single atomic + * transaction? + */ + private boolean atomic; + + private boolean allowOfsDelta; + + private boolean allowQuiet = true; + + /** Identity to record action as within the reflog. */ + private PersonIdent refLogIdent; + + /** Hook used while advertising the refs to the client. */ + private AdvertiseRefsHook advertiseRefsHook; + + /** Filter used while advertising the refs to the client. */ + private RefFilter refFilter; + + /** Timeout in seconds to wait for client interaction. */ + private int timeout; + + /** Timer to manage {@link #timeout}. */ + private InterruptTimer timer; + + private TimeoutInputStream timeoutIn; + + // Original stream passed to init(), since rawOut may be wrapped in a + // sideband. + private OutputStream origOut; + + /** Raw input stream. */ + private InputStream rawIn; + + /** Raw output stream. */ + private OutputStream rawOut; + + /** Optional message output stream. */ + private OutputStream msgOut; + + private SideBandOutputStream errOut; + + /** Packet line input stream around {@link #rawIn}. */ + private PacketLineIn pckIn; + + /** Packet line output stream around {@link #rawOut}. */ + private PacketLineOut pckOut; + + private final MessageOutputWrapper msgOutWrapper = new MessageOutputWrapper(); + + private PackParser parser; + + /** The refs we advertised as existing at the start of the connection. */ + private Map<String, Ref> refs; + + /** All SHA-1s shown to the client, which can be possible edges. */ + private Set<ObjectId> advertisedHaves; + + /** Capabilities requested by the client. */ + private Set<String> enabledCapabilities; + + String userAgent; + + private Set<ObjectId> clientShallowCommits; + + private List<ReceiveCommand> commands; + + private long maxCommandBytes; + + private long maxDiscardBytes; + + private StringBuilder advertiseError; + + /** + * If {@link BasePackPushConnection#CAPABILITY_SIDE_BAND_64K} is enabled. + */ + private boolean sideBand; + + private boolean quiet; + + /** Lock around the received pack file, while updating refs. */ + private PackLock packLock; + + private boolean checkReferencedIsReachable; + + /** Git object size limit */ + private long maxObjectSizeLimit; + + /** Total pack size limit */ + private long maxPackSizeLimit = -1; + + /** The size of the received pack, including index size */ + private Long packSize; + + private PushCertificateParser pushCertificateParser; + + private SignedPushConfig signedPushConfig; + + private PushCertificate pushCert; + + private ReceivedPackStatistics stats; + /** Hook to validate the update commands before execution. */ private PreReceiveHook preReceive; @@ -93,18 +312,120 @@ public class ReceivePack extends BaseReceivePack { * the destination repository. */ public ReceivePack(Repository into) { - super(into); + db = into; + walk = new RevWalk(db); + walk.setRetainBody(false); + + TransferConfig tc = db.getConfig().get(TransferConfig.KEY); + objectChecker = tc.newReceiveObjectChecker(); + + ReceiveConfig rc = db.getConfig().get(ReceiveConfig::new); + allowCreates = rc.allowCreates; + allowAnyDeletes = true; + allowBranchDeletes = rc.allowDeletes; + allowNonFastForwards = rc.allowNonFastForwards; + allowOfsDelta = rc.allowOfsDelta; + allowPushOptions = rc.allowPushOptions; + maxCommandBytes = rc.maxCommandBytes; + maxDiscardBytes = rc.maxDiscardBytes; + advertiseRefsHook = AdvertiseRefsHook.DEFAULT; + refFilter = RefFilter.DEFAULT; + advertisedHaves = new HashSet<>(); + clientShallowCommits = new HashSet<>(); + signedPushConfig = rc.signedPush; preReceive = PreReceiveHook.NULL; postReceive = PostReceiveHook.NULL; } + /** Configuration for receive operations. */ + private static class ReceiveConfig { + final boolean allowCreates; + + final boolean allowDeletes; + + final boolean allowNonFastForwards; + + final boolean allowOfsDelta; + + final boolean allowPushOptions; + + final long maxCommandBytes; + + final long maxDiscardBytes; + + final SignedPushConfig signedPush; + + ReceiveConfig(Config config) { + allowCreates = true; + allowDeletes = !config.getBoolean("receive", "denydeletes", false); //$NON-NLS-1$ //$NON-NLS-2$ + allowNonFastForwards = !config.getBoolean("receive", //$NON-NLS-1$ + "denynonfastforwards", false); //$NON-NLS-1$ + allowOfsDelta = config.getBoolean("repack", "usedeltabaseoffset", //$NON-NLS-1$ //$NON-NLS-2$ + true); + allowPushOptions = config.getBoolean("receive", "pushoptions", //$NON-NLS-1$ //$NON-NLS-2$ + false); + maxCommandBytes = config.getLong("receive", //$NON-NLS-1$ + "maxCommandBytes", //$NON-NLS-1$ + 3 << 20); + maxDiscardBytes = config.getLong("receive", //$NON-NLS-1$ + "maxCommandDiscardBytes", //$NON-NLS-1$ + -1); + signedPush = SignedPushConfig.KEY.parse(config); + } + } + + /** + * Output stream that wraps the current {@link #msgOut}. + * <p> + * We don't want to expose {@link #msgOut} directly because it can change + * several times over the course of a session. + */ + class MessageOutputWrapper extends OutputStream { + @Override + public void write(int ch) { + if (msgOut != null) { + try { + msgOut.write(ch); + } catch (IOException e) { + // Ignore write failures. + } + } + } + + @Override + public void write(byte[] b, int off, int len) { + if (msgOut != null) { + try { + msgOut.write(b, off, len); + } catch (IOException e) { + // Ignore write failures. + } + } + } + + @Override + public void write(byte[] b) { + write(b, 0, b.length); + } + + @Override + public void flush() { + if (msgOut != null) { + try { + msgOut.flush(); + } catch (IOException e) { + // Ignore write failures. + } + } + } + } + /** * Get the repository this receive completes into. * * @return the repository this receive completes into. */ - @Override - public final Repository getRepository() { + public Repository getRepository() { return db; } @@ -113,8 +434,7 @@ public class ReceivePack extends BaseReceivePack { * * @return the RevWalk instance used by this connection. */ - @Override - public final RevWalk getRevWalk() { + public RevWalk getRevWalk() { return walk; } @@ -124,8 +444,7 @@ public class ReceivePack extends BaseReceivePack { * @return all refs which were advertised to the client, or null if * {@link #setAdvertisedRefs(Map, Set)} has not been called yet. */ - @Override - public final Map<String, Ref> getAdvertisedRefs() { + public Map<String, Ref> getAdvertisedRefs() { return refs; } @@ -146,8 +465,8 @@ public class ReceivePack extends BaseReceivePack { * null, assumes the default set of additional haves from the * repository. */ - @Override - public void setAdvertisedRefs(Map<String, Ref> allRefs, Set<ObjectId> additionalHaves) { + public void setAdvertisedRefs(Map<String, Ref> allRefs, + Set<ObjectId> additionalHaves) { refs = allRefs != null ? allRefs : db.getAllRefs(); refs = refFilter.filter(refs); advertisedHaves.clear(); @@ -170,6 +489,1525 @@ public class ReceivePack extends BaseReceivePack { } /** + * Get objects advertised to the client. + * + * @return the set of objects advertised to the as present in this + * repository, or null if {@link #setAdvertisedRefs(Map, Set)} has + * not been called yet. + */ + public final Set<ObjectId> getAdvertisedObjects() { + return advertisedHaves; + } + + /** + * Whether this instance will validate all referenced, but not supplied by + * the client, objects are reachable from another reference. + * + * @return true if this instance will validate all referenced, but not + * supplied by the client, objects are reachable from another + * reference. + */ + public boolean isCheckReferencedObjectsAreReachable() { + return checkReferencedIsReachable; + } + + /** + * Validate all referenced but not supplied objects are reachable. + * <p> + * If enabled, this instance will verify that references to objects not + * contained within the received pack are already reachable through at least + * one other reference displayed as part of {@link #getAdvertisedRefs()}. + * <p> + * This feature is useful when the application doesn't trust the client to + * not provide a forged SHA-1 reference to an object, in an attempt to + * access parts of the DAG that they aren't allowed to see and which have + * been hidden from them via the configured + * {@link org.eclipse.jgit.transport.AdvertiseRefsHook} or + * {@link org.eclipse.jgit.transport.RefFilter}. + * <p> + * Enabling this feature may imply at least some, if not all, of the same + * functionality performed by {@link #setCheckReceivedObjects(boolean)}. + * Applications are encouraged to enable both features, if desired. + * + * @param b + * {@code true} to enable the additional check. + */ + public void setCheckReferencedObjectsAreReachable(boolean b) { + this.checkReferencedIsReachable = b; + } + + /** + * Whether this class expects a bi-directional pipe opened between the + * client and itself. + * + * @return true if this class expects a bi-directional pipe opened between + * the client and itself. The default is true. + */ + public boolean isBiDirectionalPipe() { + return biDirectionalPipe; + } + + /** + * Whether this class will assume the socket is a fully bidirectional pipe + * between the two peers and takes advantage of that by first transmitting + * the known refs, then waiting to read commands. + * + * @param twoWay + * if true, this class will assume the socket is a fully + * bidirectional pipe between the two peers and takes advantage + * of that by first transmitting the known refs, then waiting to + * read commands. If false, this class assumes it must read the + * commands before writing output and does not perform the + * initial advertising. + */ + public void setBiDirectionalPipe(boolean twoWay) { + biDirectionalPipe = twoWay; + } + + /** + * Whether there is data expected after the pack footer. + * + * @return {@code true} if there is data expected after the pack footer. + */ + public boolean isExpectDataAfterPackFooter() { + return expectDataAfterPackFooter; + } + + /** + * Whether there is additional data in InputStream after pack. + * + * @param e + * {@code true} if there is additional data in InputStream after + * pack. + */ + public void setExpectDataAfterPackFooter(boolean e) { + expectDataAfterPackFooter = e; + } + + /** + * Whether this instance will verify received objects are formatted + * correctly. + * + * @return {@code true} if this instance will verify received objects are + * formatted correctly. Validating objects requires more CPU time on + * this side of the connection. + */ + public boolean isCheckReceivedObjects() { + return objectChecker != null; + } + + /** + * Whether to enable checking received objects + * + * @param check + * {@code true} to enable checking received objects; false to + * assume all received objects are valid. + * @see #setObjectChecker(ObjectChecker) + */ + public void setCheckReceivedObjects(boolean check) { + if (check && objectChecker == null) + setObjectChecker(new ObjectChecker()); + else if (!check && objectChecker != null) + setObjectChecker(null); + } + + /** + * Set the object checking instance to verify each received object with + * + * @param impl + * if non-null the object checking instance to verify each + * received object with; null to disable object checking. + * @since 3.4 + */ + public void setObjectChecker(ObjectChecker impl) { + objectChecker = impl; + } + + /** + * Whether the client can request refs to be created. + * + * @return {@code true} if the client can request refs to be created. + */ + public boolean isAllowCreates() { + return allowCreates; + } + + /** + * Whether to permit create ref commands to be processed. + * + * @param canCreate + * {@code true} to permit create ref commands to be processed. + */ + public void setAllowCreates(boolean canCreate) { + allowCreates = canCreate; + } + + /** + * Whether the client can request refs to be deleted. + * + * @return {@code true} if the client can request refs to be deleted. + */ + public boolean isAllowDeletes() { + return allowAnyDeletes; + } + + /** + * Whether to permit delete ref commands to be processed. + * + * @param canDelete + * {@code true} to permit delete ref commands to be processed. + */ + public void setAllowDeletes(boolean canDelete) { + allowAnyDeletes = canDelete; + } + + /** + * Whether the client can delete from {@code refs/heads/}. + * + * @return {@code true} if the client can delete from {@code refs/heads/}. + * @since 3.6 + */ + public boolean isAllowBranchDeletes() { + return allowBranchDeletes; + } + + /** + * Configure whether to permit deletion of branches from the + * {@code refs/heads/} namespace. + * + * @param canDelete + * {@code true} to permit deletion of branches from the + * {@code refs/heads/} namespace. + * @since 3.6 + */ + public void setAllowBranchDeletes(boolean canDelete) { + allowBranchDeletes = canDelete; + } + + /** + * Whether the client can request non-fast-forward updates of a ref, + * possibly making objects unreachable. + * + * @return {@code true} if the client can request non-fast-forward updates + * of a ref, possibly making objects unreachable. + */ + public boolean isAllowNonFastForwards() { + return allowNonFastForwards; + } + + /** + * Configure whether to permit the client to ask for non-fast-forward + * updates of an existing ref. + * + * @param canRewind + * {@code true} to permit the client to ask for non-fast-forward + * updates of an existing ref. + */ + public void setAllowNonFastForwards(boolean canRewind) { + allowNonFastForwards = canRewind; + } + + /** + * Whether the client's commands should be performed as a single atomic + * transaction. + * + * @return {@code true} if the client's commands should be performed as a + * single atomic transaction. + * @since 4.4 + */ + public boolean isAtomic() { + return atomic; + } + + /** + * Configure whether to perform the client's commands as a single atomic + * transaction. + * + * @param atomic + * {@code true} to perform the client's commands as a single + * atomic transaction. + * @since 4.4 + */ + public void setAtomic(boolean atomic) { + this.atomic = atomic; + } + + /** + * Get identity of the user making the changes in the reflog. + * + * @return identity of the user making the changes in the reflog. + */ + public PersonIdent getRefLogIdent() { + return refLogIdent; + } + + /** + * Set the identity of the user appearing in the affected reflogs. + * <p> + * The timestamp portion of the identity is ignored. A new identity with the + * current timestamp will be created automatically when the updates occur + * and the log records are written. + * + * @param pi + * identity of the user. If null the identity will be + * automatically determined based on the repository + * configuration. + */ + public void setRefLogIdent(PersonIdent pi) { + refLogIdent = pi; + } + + /** + * Get the hook used while advertising the refs to the client + * + * @return the hook used while advertising the refs to the client + */ + public AdvertiseRefsHook getAdvertiseRefsHook() { + return advertiseRefsHook; + } + + /** + * Get the filter used while advertising the refs to the client + * + * @return the filter used while advertising the refs to the client + */ + public RefFilter getRefFilter() { + return refFilter; + } + + /** + * Set the hook used while advertising the refs to the client. + * <p> + * If the {@link org.eclipse.jgit.transport.AdvertiseRefsHook} chooses to + * call {@link #setAdvertisedRefs(Map,Set)}, only refs set by this hook + * <em>and</em> selected by the {@link org.eclipse.jgit.transport.RefFilter} + * will be shown to the client. Clients may still attempt to create or + * update a reference not advertised by the configured + * {@link org.eclipse.jgit.transport.AdvertiseRefsHook}. These attempts + * should be rejected by a matching + * {@link org.eclipse.jgit.transport.PreReceiveHook}. + * + * @param advertiseRefsHook + * the hook; may be null to show all refs. + */ + public void setAdvertiseRefsHook(AdvertiseRefsHook advertiseRefsHook) { + if (advertiseRefsHook != null) + this.advertiseRefsHook = advertiseRefsHook; + else + this.advertiseRefsHook = AdvertiseRefsHook.DEFAULT; + } + + /** + * Set the filter used while advertising the refs to the client. + * <p> + * Only refs allowed by this filter will be shown to the client. The filter + * is run against the refs specified by the + * {@link org.eclipse.jgit.transport.AdvertiseRefsHook} (if applicable). + * + * @param refFilter + * the filter; may be null to show all refs. + */ + public void setRefFilter(RefFilter refFilter) { + this.refFilter = refFilter != null ? refFilter : RefFilter.DEFAULT; + } + + /** + * Get timeout (in seconds) before aborting an IO operation. + * + * @return timeout (in seconds) before aborting an IO operation. + */ + public int getTimeout() { + return timeout; + } + + /** + * Set the timeout before willing to abort an IO call. + * + * @param seconds + * number of seconds to wait (with no data transfer occurring) + * before aborting an IO read or write operation with the + * connected client. + */ + public void setTimeout(int seconds) { + timeout = seconds; + } + + /** + * Set the maximum number of command bytes to read from the client. + * + * @param limit + * command limit in bytes; if 0 there is no limit. + * @since 4.7 + */ + public void setMaxCommandBytes(long limit) { + maxCommandBytes = limit; + } + + /** + * Set the maximum number of command bytes to discard from the client. + * <p> + * Discarding remaining bytes allows this instance to consume the rest of + * the command block and send a human readable over-limit error via the + * side-band channel. If the client sends an excessive number of bytes this + * limit kicks in and the instance disconnects, resulting in a non-specific + * 'pipe closed', 'end of stream', or similar generic error at the client. + * <p> + * When the limit is set to {@code -1} the implementation will default to + * the larger of {@code 3 * maxCommandBytes} or {@code 3 MiB}. + * + * @param limit + * discard limit in bytes; if 0 there is no limit; if -1 the + * implementation tries to set a reasonable default. + * @since 4.7 + */ + public void setMaxCommandDiscardBytes(long limit) { + maxDiscardBytes = limit; + } + + /** + * Set the maximum allowed Git object size. + * <p> + * If an object is larger than the given size the pack-parsing will throw an + * exception aborting the receive-pack operation. + * + * @param limit + * the Git object size limit. If zero then there is not limit. + */ + public void setMaxObjectSizeLimit(long limit) { + maxObjectSizeLimit = limit; + } + + /** + * Set the maximum allowed pack size. + * <p> + * A pack exceeding this size will be rejected. + * + * @param limit + * the pack size limit, in bytes + * @since 3.3 + */ + public void setMaxPackSizeLimit(long limit) { + if (limit < 0) + throw new IllegalArgumentException( + MessageFormat.format(JGitText.get().receivePackInvalidLimit, + Long.valueOf(limit))); + maxPackSizeLimit = limit; + } + + /** + * Check whether the client expects a side-band stream. + * + * @return true if the client has advertised a side-band capability, false + * otherwise. + * @throws org.eclipse.jgit.transport.RequestNotYetReadException + * if the client's request has not yet been read from the wire, + * so we do not know if they expect side-band. Note that the + * client may have already written the request, it just has not + * been read. + */ + public boolean isSideBand() throws RequestNotYetReadException { + checkRequestWasRead(); + return enabledCapabilities.contains(CAPABILITY_SIDE_BAND_64K); + } + + /** + * Whether clients may request avoiding noisy progress messages. + * + * @return true if clients may request avoiding noisy progress messages. + * @since 4.0 + */ + public boolean isAllowQuiet() { + return allowQuiet; + } + + /** + * Configure if clients may request the server skip noisy messages. + * + * @param allow + * true to allow clients to request quiet behavior; false to + * refuse quiet behavior and send messages anyway. This may be + * necessary if processing is slow and the client-server network + * connection can timeout. + * @since 4.0 + */ + public void setAllowQuiet(boolean allow) { + allowQuiet = allow; + } + + /** + * Whether the server supports receiving push options. + * + * @return true if the server supports receiving push options. + * @since 4.5 + */ + public boolean isAllowPushOptions() { + return allowPushOptions; + } + + /** + * Configure if the server supports receiving push options. + * + * @param allow + * true to optionally accept option strings from the client. + * @since 4.5 + */ + public void setAllowPushOptions(boolean allow) { + allowPushOptions = allow; + } + + /** + * True if the client wants less verbose output. + * + * @return true if the client has requested the server to be less verbose. + * @throws org.eclipse.jgit.transport.RequestNotYetReadException + * if the client's request has not yet been read from the wire, + * so we do not know if they expect side-band. Note that the + * client may have already written the request, it just has not + * been read. + * @since 4.0 + */ + public boolean isQuiet() throws RequestNotYetReadException { + checkRequestWasRead(); + return quiet; + } + + /** + * Set the configuration for push certificate verification. + * + * @param cfg + * new configuration; if this object is null or its + * {@link SignedPushConfig#getCertNonceSeed()} is null, push + * certificate verification will be disabled. + * @since 4.1 + */ + public void setSignedPushConfig(SignedPushConfig cfg) { + signedPushConfig = cfg; + } + + private PushCertificateParser getPushCertificateParser() { + if (pushCertificateParser == null) { + pushCertificateParser = new PushCertificateParser(db, + signedPushConfig); + } + return pushCertificateParser; + } + + /** + * Get the user agent of the client. + * <p> + * If the client is new enough to use {@code agent=} capability that value + * will be returned. Older HTTP clients may also supply their version using + * the HTTP {@code User-Agent} header. The capability overrides the HTTP + * header if both are available. + * <p> + * When an HTTP request has been received this method returns the HTTP + * {@code User-Agent} header value until capabilities have been parsed. + * + * @return user agent supplied by the client. Available only if the client + * is new enough to advertise its user agent. + * @since 4.0 + */ + public String getPeerUserAgent() { + return UserAgent.getAgent(enabledCapabilities, userAgent); + } + + /** + * Get all of the command received by the current request. + * + * @return all of the command received by the current request. + */ + public List<ReceiveCommand> getAllCommands() { + return Collections.unmodifiableList(commands); + } + + /** + * Send an error message to the client. + * <p> + * If any error messages are sent before the references are advertised to + * the client, the errors will be sent instead of the advertisement and the + * receive operation will be aborted. All clients should receive and display + * such early stage errors. + * <p> + * If the reference advertisements have already been sent, messages are sent + * in a side channel. If the client doesn't support receiving messages, the + * message will be discarded, with no other indication to the caller or to + * the client. + * <p> + * {@link org.eclipse.jgit.transport.PreReceiveHook}s should always try to + * use + * {@link org.eclipse.jgit.transport.ReceiveCommand#setResult(Result, String)} + * with a result status of + * {@link org.eclipse.jgit.transport.ReceiveCommand.Result#REJECTED_OTHER_REASON} + * to indicate any reasons for rejecting an update. Messages attached to a + * command are much more likely to be returned to the client. + * + * @param what + * string describing the problem identified by the hook. The + * string must not end with an LF, and must not contain an LF. + */ + public void sendError(String what) { + if (refs == null) { + if (advertiseError == null) + advertiseError = new StringBuilder(); + advertiseError.append(what).append('\n'); + } else { + msgOutWrapper.write(Constants.encode("error: " + what + "\n")); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + private void fatalError(String msg) { + if (errOut != null) { + try { + errOut.write(Constants.encode(msg)); + errOut.flush(); + } catch (IOException e) { + // Ignore write failures + } + } else { + sendError(msg); + } + } + + /** + * Send a message to the client, if it supports receiving them. + * <p> + * If the client doesn't support receiving messages, the message will be + * discarded, with no other indication to the caller or to the client. + * + * @param what + * string describing the problem identified by the hook. The + * string must not end with an LF, and must not contain an LF. + */ + public void sendMessage(String what) { + msgOutWrapper.write(Constants.encode(what + "\n")); //$NON-NLS-1$ + } + + /** + * Get an underlying stream for sending messages to the client. + * + * @return an underlying stream for sending messages to the client. + */ + public OutputStream getMessageOutputStream() { + return msgOutWrapper; + } + + /** + * Get whether or not a pack has been received. + * + * This can be called before calling {@link #getPackSize()} to avoid causing + * {@code IllegalStateException} when the pack size was not set because no + * pack was received. + * + * @return true if a pack has been received. + * @since 5.6 + */ + public boolean hasReceivedPack() { + return packSize != null; + } + + /** + * Get the size of the received pack file including the index size. + * + * This can only be called if the pack is already received. + * + * @return the size of the received pack including index size + * @throws java.lang.IllegalStateException + * if called before the pack has been received + * @since 3.3 + */ + public long getPackSize() { + if (packSize != null) + return packSize.longValue(); + throw new IllegalStateException(JGitText.get().packSizeNotSetYet); + } + + /** + * Get the commits from the client's shallow file. + * + * @return if the client is a shallow repository, the list of edge commits + * that define the client's shallow boundary. Empty set if the + * client is earlier than Git 1.9, or is a full clone. + */ + private Set<ObjectId> getClientShallowCommits() { + return clientShallowCommits; + } + + /** + * Whether any commands to be executed have been read. + * + * @return {@code true} if any commands to be executed have been read. + */ + private boolean hasCommands() { + return !commands.isEmpty(); + } + + /** + * Whether an error occurred that should be advertised. + * + * @return true if an error occurred that should be advertised. + */ + private boolean hasError() { + return advertiseError != null; + } + + /** + * Initialize the instance with the given streams. + * + * Visible for out-of-tree subclasses (e.g. tests that need to set the + * streams without going through the {@link #service()} method). + * + * @param input + * raw input to read client commands and pack data from. Caller + * must ensure the input is buffered, otherwise read performance + * may suffer. + * @param output + * response back to the Git network client. Caller must ensure + * the output is buffered, otherwise write performance may + * suffer. + * @param messages + * secondary "notice" channel to send additional messages out + * through. When run over SSH this should be tied back to the + * standard error channel of the command execution. For most + * other network connections this should be null. + */ + protected void init(final InputStream input, final OutputStream output, + final OutputStream messages) { + origOut = output; + rawIn = input; + rawOut = output; + msgOut = messages; + + if (timeout > 0) { + final Thread caller = Thread.currentThread(); + timer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$ + timeoutIn = new TimeoutInputStream(rawIn, timer); + TimeoutOutputStream o = new TimeoutOutputStream(rawOut, timer); + timeoutIn.setTimeout(timeout * 1000); + o.setTimeout(timeout * 1000); + rawIn = timeoutIn; + rawOut = o; + } + + pckIn = new PacketLineIn(rawIn); + pckOut = new PacketLineOut(rawOut); + pckOut.setFlushOnEnd(false); + + enabledCapabilities = new HashSet<>(); + commands = new ArrayList<>(); + } + + /** + * Get advertised refs, or the default if not explicitly advertised. + * + * @return advertised refs, or the default if not explicitly advertised. + */ + private Map<String, Ref> getAdvertisedOrDefaultRefs() { + if (refs == null) + setAdvertisedRefs(null, null); + return refs; + } + + /** + * Receive a pack from the stream and check connectivity if necessary. + * + * Visible for out-of-tree subclasses. Subclasses overriding this method + * should invoke this implementation, as it alters the instance state (e.g. + * it reads the pack from the input and parses it before running the + * connectivity checks). + * + * @throws java.io.IOException + * an error occurred during unpacking or connectivity checking. + */ + protected void receivePackAndCheckConnectivity() throws IOException { + receivePack(); + if (needCheckConnectivity()) { + checkSubmodules(); + checkConnectivity(); + } + parser = null; + } + + /** + * Unlock the pack written by this object. + * + * @throws java.io.IOException + * the pack could not be unlocked. + */ + private void unlockPack() throws IOException { + if (packLock != null) { + packLock.unlock(); + packLock = null; + } + } + + /** + * Generate an advertisement of available refs and capabilities. + * + * @param adv + * the advertisement formatter. + * @throws java.io.IOException + * the formatter failed to write an advertisement. + * @throws org.eclipse.jgit.transport.ServiceMayNotContinueException + * the hook denied advertisement. + */ + public void sendAdvertisedRefs(RefAdvertiser adv) + throws IOException, ServiceMayNotContinueException { + if (advertiseError != null) { + adv.writeOne("ERR " + advertiseError); //$NON-NLS-1$ + return; + } + + try { + advertiseRefsHook.advertiseRefs(this); + } catch (ServiceMayNotContinueException fail) { + if (fail.getMessage() != null) { + adv.writeOne("ERR " + fail.getMessage()); //$NON-NLS-1$ + fail.setOutput(); + } + throw fail; + } + + adv.init(db); + adv.advertiseCapability(CAPABILITY_SIDE_BAND_64K); + adv.advertiseCapability(CAPABILITY_DELETE_REFS); + adv.advertiseCapability(CAPABILITY_REPORT_STATUS); + if (allowQuiet) + adv.advertiseCapability(CAPABILITY_QUIET); + String nonce = getPushCertificateParser().getAdvertiseNonce(); + if (nonce != null) { + adv.advertiseCapability(nonce); + } + if (db.getRefDatabase().performsAtomicTransactions()) + adv.advertiseCapability(CAPABILITY_ATOMIC); + if (allowOfsDelta) + adv.advertiseCapability(CAPABILITY_OFS_DELTA); + if (allowPushOptions) { + adv.advertiseCapability(CAPABILITY_PUSH_OPTIONS); + } + adv.advertiseCapability(OPTION_AGENT, UserAgent.get()); + adv.send(getAdvertisedOrDefaultRefs().values()); + for (ObjectId obj : advertisedHaves) + adv.advertiseHave(obj); + if (adv.isEmpty()) + adv.advertiseId(ObjectId.zeroId(), "capabilities^{}"); //$NON-NLS-1$ + adv.end(); + } + + /** + * Returns the statistics on the received pack if available. This should be + * called after {@link #receivePack} is called. + * + * @return ReceivedPackStatistics + * @since 4.6 + */ + @Nullable + public ReceivedPackStatistics getReceivedPackStatistics() { + return stats; + } + + /** + * Receive a list of commands from the input. + * + * @throws java.io.IOException + */ + private void recvCommands() throws IOException { + PacketLineIn pck = maxCommandBytes > 0 + ? new PacketLineIn(rawIn, maxCommandBytes) + : pckIn; + PushCertificateParser certParser = getPushCertificateParser(); + boolean firstPkt = true; + try { + for (;;) { + String line; + try { + line = pck.readString(); + } catch (EOFException eof) { + if (commands.isEmpty()) + return; + throw eof; + } + if (PacketLineIn.isEnd(line)) { + break; + } + + if (line.length() >= 48 && line.startsWith("shallow ")) { //$NON-NLS-1$ + parseShallow(line.substring(8, 48)); + continue; + } + + if (firstPkt) { + firstPkt = false; + FirstCommand firstLine = FirstCommand.fromLine(line); + enabledCapabilities = firstLine.getCapabilities(); + line = firstLine.getLine(); + enableCapabilities(); + + if (line.equals(GitProtocolConstants.OPTION_PUSH_CERT)) { + certParser.receiveHeader(pck, !isBiDirectionalPipe()); + continue; + } + } + + if (line.equals(PushCertificateParser.BEGIN_SIGNATURE)) { + certParser.receiveSignature(pck); + continue; + } + + ReceiveCommand cmd = parseCommand(line); + if (cmd.getRefName().equals(Constants.HEAD)) { + cmd.setResult(Result.REJECTED_CURRENT_BRANCH); + } else { + cmd.setRef(refs.get(cmd.getRefName())); + } + commands.add(cmd); + if (certParser.enabled()) { + certParser.addCommand(cmd); + } + } + pushCert = certParser.build(); + if (hasCommands()) { + readPostCommands(pck); + } + } catch (PackProtocolException e) { + discardCommands(); + fatalError(e.getMessage()); + throw e; + } catch (InputOverLimitIOException e) { + String msg = JGitText.get().tooManyCommands; + discardCommands(); + fatalError(msg); + throw new PackProtocolException(msg); + } + } + + private void discardCommands() { + if (sideBand) { + long max = maxDiscardBytes; + if (max < 0) { + max = Math.max(3 * maxCommandBytes, 3L << 20); + } + try { + new PacketLineIn(rawIn, max).discardUntilEnd(); + } catch (IOException e) { + // Ignore read failures attempting to discard. + } + } + } + + private void parseShallow(String idStr) throws PackProtocolException { + ObjectId id; + try { + id = ObjectId.fromString(idStr); + } catch (InvalidObjectIdException e) { + throw new PackProtocolException(e.getMessage(), e); + } + clientShallowCommits.add(id); + } + + /** + * @param in + * request stream. + * @throws IOException + * request line cannot be read. + */ + void readPostCommands(PacketLineIn in) throws IOException { + if (usePushOptions) { + pushOptions = new ArrayList<>(4); + for (;;) { + String option = in.readString(); + if (PacketLineIn.isEnd(option)) { + break; + } + pushOptions.add(option); + } + } + } + + /** + * Enable capabilities based on a previously read capabilities line. + */ + private void enableCapabilities() { + reportStatus = isCapabilityEnabled(CAPABILITY_REPORT_STATUS); + usePushOptions = isCapabilityEnabled(CAPABILITY_PUSH_OPTIONS); + sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K); + quiet = allowQuiet && isCapabilityEnabled(CAPABILITY_QUIET); + if (sideBand) { + OutputStream out = rawOut; + + rawOut = new SideBandOutputStream(CH_DATA, MAX_BUF, out); + msgOut = new SideBandOutputStream(CH_PROGRESS, MAX_BUF, out); + errOut = new SideBandOutputStream(CH_ERROR, MAX_BUF, out); + + pckOut = new PacketLineOut(rawOut); + pckOut.setFlushOnEnd(false); + } + } + + /** + * Check if the peer requested a capability. + * + * @param name + * protocol name identifying the capability. + * @return true if the peer requested the capability to be enabled. + */ + private boolean isCapabilityEnabled(String name) { + return enabledCapabilities.contains(name); + } + + private void checkRequestWasRead() { + if (enabledCapabilities == null) + throw new RequestNotYetReadException(); + } + + /** + * Whether a pack is expected based on the list of commands. + * + * @return {@code true} if a pack is expected based on the list of commands. + */ + private boolean needPack() { + for (ReceiveCommand cmd : commands) { + if (cmd.getType() != ReceiveCommand.Type.DELETE) + return true; + } + return false; + } + + /** + * Receive a pack from the input and store it in the repository. + * + * @throws IOException + * an error occurred reading or indexing the pack. + */ + private void receivePack() throws IOException { + // It might take the client a while to pack the objects it needs + // to send to us. We should increase our timeout so we don't + // abort while the client is computing. + // + if (timeoutIn != null) + timeoutIn.setTimeout(10 * timeout * 1000); + + ProgressMonitor receiving = NullProgressMonitor.INSTANCE; + ProgressMonitor resolving = NullProgressMonitor.INSTANCE; + if (sideBand && !quiet) + resolving = new SideBandProgressMonitor(msgOut); + + try (ObjectInserter ins = db.newObjectInserter()) { + String lockMsg = "jgit receive-pack"; //$NON-NLS-1$ + if (getRefLogIdent() != null) + lockMsg += " from " + getRefLogIdent().toExternalString(); //$NON-NLS-1$ + + parser = ins.newPackParser(packInputStream()); + parser.setAllowThin(true); + parser.setNeedNewObjectIds(checkReferencedIsReachable); + parser.setNeedBaseObjectIds(checkReferencedIsReachable); + parser.setCheckEofAfterPackFooter( + !biDirectionalPipe && !isExpectDataAfterPackFooter()); + parser.setExpectDataAfterPackFooter(isExpectDataAfterPackFooter()); + parser.setObjectChecker(objectChecker); + parser.setLockMessage(lockMsg); + parser.setMaxObjectSizeLimit(maxObjectSizeLimit); + packLock = parser.parse(receiving, resolving); + packSize = Long.valueOf(parser.getPackSize()); + stats = parser.getReceivedPackStatistics(); + ins.flush(); + } + + if (timeoutIn != null) + timeoutIn.setTimeout(timeout * 1000); + } + + private InputStream packInputStream() { + InputStream packIn = rawIn; + if (maxPackSizeLimit >= 0) { + packIn = new LimitedInputStream(packIn, maxPackSizeLimit) { + @Override + protected void limitExceeded() throws TooLargePackException { + throw new TooLargePackException(limit); + } + }; + } + return packIn; + } + + private boolean needCheckConnectivity() { + return isCheckReceivedObjects() + || isCheckReferencedObjectsAreReachable() + || !getClientShallowCommits().isEmpty(); + } + + private void checkSubmodules() throws IOException { + ObjectDatabase odb = db.getObjectDatabase(); + if (objectChecker == null) { + return; + } + for (GitmoduleEntry entry : objectChecker.getGitsubmodules()) { + AnyObjectId blobId = entry.getBlobId(); + ObjectLoader blob = odb.open(blobId, Constants.OBJ_BLOB); + + try { + SubmoduleValidator.assertValidGitModulesFile( + new String(blob.getBytes(), UTF_8)); + } catch (LargeObjectException | SubmoduleValidationException e) { + throw new IOException(e); + } + } + } + + private void checkConnectivity() throws IOException { + ObjectIdSubclassMap<ObjectId> baseObjects = null; + ObjectIdSubclassMap<ObjectId> providedObjects = null; + ProgressMonitor checking = NullProgressMonitor.INSTANCE; + if (sideBand && !quiet) { + SideBandProgressMonitor m = new SideBandProgressMonitor(msgOut); + m.setDelayStart(750, TimeUnit.MILLISECONDS); + checking = m; + } + + if (checkReferencedIsReachable) { + baseObjects = parser.getBaseObjectIds(); + providedObjects = parser.getNewObjectIds(); + } + parser = null; + + try (ObjectWalk ow = new ObjectWalk(db)) { + if (baseObjects != null) { + ow.sort(RevSort.TOPO); + if (!baseObjects.isEmpty()) + ow.sort(RevSort.BOUNDARY, true); + } + + for (ReceiveCommand cmd : commands) { + if (cmd.getResult() != Result.NOT_ATTEMPTED) + continue; + if (cmd.getType() == ReceiveCommand.Type.DELETE) + continue; + ow.markStart(ow.parseAny(cmd.getNewId())); + } + for (ObjectId have : advertisedHaves) { + RevObject o = ow.parseAny(have); + ow.markUninteresting(o); + + if (baseObjects != null && !baseObjects.isEmpty()) { + o = ow.peel(o); + if (o instanceof RevCommit) + o = ((RevCommit) o).getTree(); + if (o instanceof RevTree) + ow.markUninteresting(o); + } + } + + checking.beginTask(JGitText.get().countingObjects, + ProgressMonitor.UNKNOWN); + RevCommit c; + while ((c = ow.next()) != null) { + checking.update(1); + if (providedObjects != null // + && !c.has(RevFlag.UNINTERESTING) // + && !providedObjects.contains(c)) + throw new MissingObjectException(c, Constants.TYPE_COMMIT); + } + + RevObject o; + while ((o = ow.nextObject()) != null) { + checking.update(1); + if (o.has(RevFlag.UNINTERESTING)) + continue; + + if (providedObjects != null) { + if (providedObjects.contains(o)) { + continue; + } + throw new MissingObjectException(o, o.getType()); + } + + if (o instanceof RevBlob && !db.getObjectDatabase().has(o)) + throw new MissingObjectException(o, Constants.TYPE_BLOB); + } + checking.endTask(); + + if (baseObjects != null) { + for (ObjectId id : baseObjects) { + o = ow.parseAny(id); + if (!o.has(RevFlag.UNINTERESTING)) + throw new MissingObjectException(o, o.getType()); + } + } + } + } + + /** + * Validate the command list. + */ + private void validateCommands() { + for (ReceiveCommand cmd : commands) { + final Ref ref = cmd.getRef(); + if (cmd.getResult() != Result.NOT_ATTEMPTED) + continue; + + if (cmd.getType() == ReceiveCommand.Type.DELETE) { + if (!isAllowDeletes()) { + // Deletes are not supported on this repository. + cmd.setResult(Result.REJECTED_NODELETE); + continue; + } + if (!isAllowBranchDeletes() + && ref.getName().startsWith(Constants.R_HEADS)) { + // Branches cannot be deleted, but other refs can. + cmd.setResult(Result.REJECTED_NODELETE); + continue; + } + } + + if (cmd.getType() == ReceiveCommand.Type.CREATE) { + if (!isAllowCreates()) { + cmd.setResult(Result.REJECTED_NOCREATE); + continue; + } + + if (ref != null && !isAllowNonFastForwards()) { + // Creation over an existing ref is certainly not going + // to be a fast-forward update. We can reject it early. + // + cmd.setResult(Result.REJECTED_NONFASTFORWARD); + continue; + } + + if (ref != null) { + // A well behaved client shouldn't have sent us a + // create command for a ref we advertised to it. + // + cmd.setResult(Result.REJECTED_OTHER_REASON, + JGitText.get().refAlreadyExists); + continue; + } + } + + if (cmd.getType() == ReceiveCommand.Type.DELETE && ref != null) { + ObjectId id = ref.getObjectId(); + if (id == null) { + id = ObjectId.zeroId(); + } + if (!ObjectId.zeroId().equals(cmd.getOldId()) + && !id.equals(cmd.getOldId())) { + // Delete commands can be sent with the old id matching our + // advertised value, *OR* with the old id being 0{40}. Any + // other requested old id is invalid. + // + cmd.setResult(Result.REJECTED_OTHER_REASON, + JGitText.get().invalidOldIdSent); + continue; + } + } + + if (cmd.getType() == ReceiveCommand.Type.UPDATE) { + if (ref == null) { + // The ref must have been advertised in order to be updated. + // + cmd.setResult(Result.REJECTED_OTHER_REASON, + JGitText.get().noSuchRef); + continue; + } + ObjectId id = ref.getObjectId(); + if (id == null) { + // We cannot update unborn branch + cmd.setResult(Result.REJECTED_OTHER_REASON, + JGitText.get().cannotUpdateUnbornBranch); + continue; + } + + if (!id.equals(cmd.getOldId())) { + // A properly functioning client will send the same + // object id we advertised. + // + cmd.setResult(Result.REJECTED_OTHER_REASON, + JGitText.get().invalidOldIdSent); + continue; + } + + // Is this possibly a non-fast-forward style update? + // + RevObject oldObj, newObj; + try { + oldObj = walk.parseAny(cmd.getOldId()); + } catch (IOException e) { + cmd.setResult(Result.REJECTED_MISSING_OBJECT, + cmd.getOldId().name()); + continue; + } + + try { + newObj = walk.parseAny(cmd.getNewId()); + } catch (IOException e) { + cmd.setResult(Result.REJECTED_MISSING_OBJECT, + cmd.getNewId().name()); + continue; + } + + if (oldObj instanceof RevCommit + && newObj instanceof RevCommit) { + try { + if (walk.isMergedInto((RevCommit) oldObj, + (RevCommit) newObj)) + cmd.setTypeFastForwardUpdate(); + else + cmd.setType( + ReceiveCommand.Type.UPDATE_NONFASTFORWARD); + } catch (MissingObjectException e) { + cmd.setResult(Result.REJECTED_MISSING_OBJECT, + e.getMessage()); + } catch (IOException e) { + cmd.setResult(Result.REJECTED_OTHER_REASON); + } + } else { + cmd.setType(ReceiveCommand.Type.UPDATE_NONFASTFORWARD); + } + + if (cmd.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD + && !isAllowNonFastForwards()) { + cmd.setResult(Result.REJECTED_NONFASTFORWARD); + continue; + } + } + + if (!cmd.getRefName().startsWith(Constants.R_REFS) + || !Repository.isValidRefName(cmd.getRefName())) { + cmd.setResult(Result.REJECTED_OTHER_REASON, + JGitText.get().funnyRefname); + } + } + } + + /** + * Whether any commands have been rejected so far. + * + * @return if any commands have been rejected so far. + */ + private boolean anyRejects() { + for (ReceiveCommand cmd : commands) { + if (cmd.getResult() != Result.NOT_ATTEMPTED + && cmd.getResult() != Result.OK) + return true; + } + return false; + } + + /** + * Set the result to fail for any command that was not processed yet. + * + */ + private void failPendingCommands() { + ReceiveCommand.abort(commands); + } + + /** + * Filter the list of commands according to result. + * + * @param want + * desired status to filter by. + * @return a copy of the command list containing only those commands with + * the desired status. + */ + private List<ReceiveCommand> filterCommands(Result want) { + return ReceiveCommand.filter(commands, want); + } + + /** + * Execute commands to update references. + */ + private void executeCommands() { + List<ReceiveCommand> toApply = filterCommands(Result.NOT_ATTEMPTED); + if (toApply.isEmpty()) + return; + + ProgressMonitor updating = NullProgressMonitor.INSTANCE; + if (sideBand) { + SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut); + pm.setDelayStart(250, TimeUnit.MILLISECONDS); + updating = pm; + } + + BatchRefUpdate batch = db.getRefDatabase().newBatchUpdate(); + batch.setAllowNonFastForwards(isAllowNonFastForwards()); + batch.setAtomic(isAtomic()); + batch.setRefLogIdent(getRefLogIdent()); + batch.setRefLogMessage("push", true); //$NON-NLS-1$ + batch.addCommand(toApply); + try { + batch.setPushCertificate(getPushCertificate()); + batch.execute(walk, updating); + } catch (IOException err) { + for (ReceiveCommand cmd : toApply) { + if (cmd.getResult() == Result.NOT_ATTEMPTED) + cmd.reject(err); + } + } + } + + /** + * Send a status report. + * + * @param forClient + * true if this report is for a Git client, false if it is for an + * end-user. + * @param unpackError + * an error that occurred during unpacking, or {@code null} + * @param out + * the reporter for sending the status strings. + * @throws java.io.IOException + * an error occurred writing the status report. + * @since 5.6 + */ + private void sendStatusReport(final boolean forClient, + final Throwable unpackError, final Reporter out) + throws IOException { + if (unpackError != null) { + out.sendString("unpack error " + unpackError.getMessage()); //$NON-NLS-1$ + if (forClient) { + for (ReceiveCommand cmd : commands) { + out.sendString("ng " + cmd.getRefName() //$NON-NLS-1$ + + " n/a (unpacker error)"); //$NON-NLS-1$ + } + } + return; + } + + if (forClient) + out.sendString("unpack ok"); //$NON-NLS-1$ + for (ReceiveCommand cmd : commands) { + if (cmd.getResult() == Result.OK) { + if (forClient) + out.sendString("ok " + cmd.getRefName()); //$NON-NLS-1$ + continue; + } + + final StringBuilder r = new StringBuilder(); + if (forClient) + r.append("ng ").append(cmd.getRefName()).append(" "); //$NON-NLS-1$ //$NON-NLS-2$ + else + r.append(" ! [rejected] ").append(cmd.getRefName()) //$NON-NLS-1$ + .append(" ("); //$NON-NLS-1$ + + switch (cmd.getResult()) { + case NOT_ATTEMPTED: + r.append("server bug; ref not processed"); //$NON-NLS-1$ + break; + + case REJECTED_NOCREATE: + r.append("creation prohibited"); //$NON-NLS-1$ + break; + + case REJECTED_NODELETE: + r.append("deletion prohibited"); //$NON-NLS-1$ + break; + + case REJECTED_NONFASTFORWARD: + r.append("non-fast forward"); //$NON-NLS-1$ + break; + + case REJECTED_CURRENT_BRANCH: + r.append("branch is currently checked out"); //$NON-NLS-1$ + break; + + case REJECTED_MISSING_OBJECT: + if (cmd.getMessage() == null) + r.append("missing object(s)"); //$NON-NLS-1$ + else if (cmd.getMessage() + .length() == Constants.OBJECT_ID_STRING_LENGTH) { + r.append("object "); //$NON-NLS-1$ + r.append(cmd.getMessage()); + r.append(" missing"); //$NON-NLS-1$ + } else + r.append(cmd.getMessage()); + break; + + case REJECTED_OTHER_REASON: + if (cmd.getMessage() == null) + r.append("unspecified reason"); //$NON-NLS-1$ + else + r.append(cmd.getMessage()); + break; + + case LOCK_FAILURE: + r.append("failed to lock"); //$NON-NLS-1$ + break; + + case OK: + // We shouldn't have reached this case (see 'ok' case above). + continue; + } + if (!forClient) + r.append(")"); //$NON-NLS-1$ + out.sendString(r.toString()); + } + } + + /** + * Close and flush (if necessary) the underlying streams. + * + * @throws java.io.IOException + */ + private void close() throws IOException { + if (sideBand) { + // If we are using side band, we need to send a final + // flush-pkt to tell the remote peer the side band is + // complete and it should stop decoding. We need to + // use the original output stream as rawOut is now the + // side band data channel. + // + ((SideBandOutputStream) msgOut).flushBuffer(); + ((SideBandOutputStream) rawOut).flushBuffer(); + + PacketLineOut plo = new PacketLineOut(origOut); + plo.setFlushOnEnd(false); + plo.end(); + } + + if (biDirectionalPipe) { + // If this was a native git connection, flush the pipe for + // the caller. For smart HTTP we don't do this flush and + // instead let the higher level HTTP servlet code do it. + // + if (!sideBand && msgOut != null) + msgOut.flush(); + rawOut.flush(); + } + } + + /** + * Release any resources used by this object. + * + * @throws java.io.IOException + * the pack could not be unlocked. + */ + private void release() throws IOException { + walk.close(); + unlockPack(); + timeoutIn = null; + rawIn = null; + rawOut = null; + msgOut = null; + pckIn = null; + pckOut = null; + refs = null; + // Keep the capabilities. If responses are sent after this release + // we need to remember at least whether sideband communication has to be + // used + commands = null; + if (timer != null) { + try { + timer.terminate(); + } finally { + timer = null; + } + } + } + + /** Interface for reporting status messages. */ + abstract static class Reporter { + abstract void sendString(String s) throws IOException; + } + + /** * Get the push certificate used to verify the pusher's identity. * <p> * Only valid after commands are read from the wire. @@ -178,7 +2016,6 @@ public class ReceivePack extends BaseReceivePack { * or no cert was presented by the client. * @since 4.1 */ - @Override public PushCertificate getPushCertificate() { return pushCert; } @@ -193,7 +2030,6 @@ public class ReceivePack extends BaseReceivePack { * the push certificate to set. * @since 4.1 */ - @Override public void setPushCertificate(PushCertificate cert) { pushCert = cert; } @@ -334,28 +2170,6 @@ public class ReceivePack extends BaseReceivePack { } } - /** {@inheritDoc} */ - @Override - protected void enableCapabilities() { - reportStatus = isCapabilityEnabled(CAPABILITY_REPORT_STATUS); - usePushOptions = isCapabilityEnabled(CAPABILITY_PUSH_OPTIONS); - super.enableCapabilities(); - } - - @Override - void readPostCommands(PacketLineIn in) throws IOException { - if (usePushOptions) { - pushOptions = new ArrayList<>(4); - for (;;) { - String option = in.readString(); - if (PacketLineIn.isEnd(option)) { - break; - } - pushOptions.add(option); - } - } - } - private void service() throws IOException { if (isBiDirectionalPipe()) { sendAdvertisedRefs(new PacketLineOutRefAdvertiser(pckOut)); @@ -377,8 +2191,7 @@ public class ReceivePack extends BaseReceivePack { try { if (unpackError == null) { - boolean atomic = isCapabilityEnabled(CAPABILITY_ATOMIC); - setAtomic(atomic); + setAtomic(isCapabilityEnabled(CAPABILITY_ATOMIC)); validateCommands(); if (atomic && anyRejects()) { @@ -437,9 +2250,27 @@ public class ReceivePack extends BaseReceivePack { repo.autoGC(NullProgressMonitor.INSTANCE); } - /** {@inheritDoc} */ - @Override - protected String getLockMessageProcessName() { - return "jgit receive-pack"; //$NON-NLS-1$ + static ReceiveCommand parseCommand(String line) + throws PackProtocolException { + if (line == null || line.length() < 83) { + throw new PackProtocolException( + JGitText.get().errorInvalidProtocolWantedOldNewRef); + } + String oldStr = line.substring(0, 40); + String newStr = line.substring(41, 81); + ObjectId oldId, newId; + try { + oldId = ObjectId.fromString(oldStr); + newId = ObjectId.fromString(newStr); + } catch (InvalidObjectIdException e) { + throw new PackProtocolException( + JGitText.get().errorInvalidProtocolWantedOldNewRef, e); + } + String name = line.substring(82); + if (!Repository.isValidRefName(name)) { + throw new PackProtocolException( + JGitText.get().errorInvalidProtocolWantedOldNewRef); + } + return new ReceiveCommand(oldId, newId, name); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java index 0a621f19e1..4474d1a858 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java @@ -292,22 +292,26 @@ public class RemoteConfig implements Serializable { private String replaceUri(final String uri, final Map<String, String> replacements) { - if (replacements.isEmpty()) + if (replacements.isEmpty()) { return uri; + } Entry<String, String> match = null; for (Entry<String, String> replacement : replacements.entrySet()) { // Ignore current entry if not longer than previous match if (match != null - && match.getKey().length() > replacement.getKey().length()) + && match.getKey().length() > replacement.getKey() + .length()) { continue; - if (!uri.startsWith(replacement.getKey())) + } + if (!uri.startsWith(replacement.getKey())) { continue; + } match = replacement; } - if (match != null) + if (match != null) { return match.getValue() + uri.substring(match.getKey().length()); - else - return uri; + } + return uri; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java index c34e3b8e61..faa2c76988 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java @@ -72,7 +72,7 @@ public class RemoteRefUpdate { /** * Represent current status of a remote ref update. */ - public static enum Status { + public enum Status { /** * Push process hasn't yet attempted to update this ref. This is the * default status, prior to push process execution. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java index 758d74c3fa..100b433e00 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java @@ -132,6 +132,7 @@ public class TransferConfig { private final boolean allowReachableSha1InWant; private final boolean allowFilter; private final boolean allowSidebandAll; + private final boolean advertiseSidebandAll; final @Nullable ProtocolVersion protocolVersion; final String[] hideRefs; @@ -213,6 +214,8 @@ public class TransferConfig { hideRefs = rc.getStringList("uploadpack", null, "hiderefs"); allowSidebandAll = rc.getBoolean( "uploadpack", "allowsidebandall", false); + advertiseSidebandAll = rc.getBoolean("uploadpack", + "advertisesidebandall", false); } /** @@ -295,7 +298,8 @@ public class TransferConfig { } /** - * @return true if clients are allowed to specify a "sideband-all" line + * @return true if the server accepts sideband-all requests (see + * {{@link #isAdvertiseSidebandAll()} for the advertisement) * @since 5.5 */ public boolean isAllowSidebandAll() { @@ -303,6 +307,14 @@ public class TransferConfig { } /** + * @return true to advertise sideband all to the clients + * @since 5.6 + */ + public boolean isAdvertiseSidebandAll() { + return advertiseSidebandAll && allowSidebandAll; + } + + /** * Get {@link org.eclipse.jgit.transport.RefFilter} respecting configured * hidden refs. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java index 0b7907035e..2382f4c3af 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -302,11 +302,12 @@ public abstract class Transport implements AutoCloseable { URISyntaxException, TransportException { if (local != null) { final RemoteConfig cfg = new RemoteConfig(local.getConfig(), remote); - if (doesNotExist(cfg)) + if (doesNotExist(cfg)) { return open(local, new URIish(remote), null); + } return open(local, cfg, op); - } else - return open(new URIish(remote)); + } + return open(new URIish(remote)); } @@ -708,11 +709,11 @@ public abstract class Transport implements AutoCloseable { // try to find matching tracking refs for (RefSpec fetchSpec : fetchSpecs) { if (fetchSpec.matchSource(remoteName)) { - if (fetchSpec.isWildcard()) + if (fetchSpec.isWildcard()) { return fetchSpec.expandFromSource(remoteName) .getDestination(); - else - return fetchSpec.getDestination(); + } + return fetchSpec.getDestination(); } } return null; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java index 3029327c52..2637a0273a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -698,9 +698,8 @@ public class TransportHttp extends HttpTransport implements WalkTransport, public CredentialItem[] items() { if (forRepo == null) { return new CredentialItem[] { message, now, always }; - } else { - return new CredentialItem[] { message, now, forRepo, always }; } + return new CredentialItem[] { message, now, forRepo, always }; } } @@ -1076,13 +1075,11 @@ public class TransportHttp extends HttpTransport implements WalkTransport, host = host.toLowerCase(Locale.ROOT); if (host.equals(cookieDomain)) { return true; - } else { - if (!host.endsWith(cookieDomain)) { - return false; - } - return host - .charAt(host.length() - cookieDomain.length() - 1) == '.'; } + if (!host.endsWith(cookieDomain)) { + return false; + } + return host.charAt(host.length() - cookieDomain.length() - 1) == '.'; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java index de58aa9e1b..5a8cd32a90 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java @@ -90,7 +90,7 @@ import org.eclipse.jgit.lib.Repository; */ public abstract class TransportProtocol { /** Fields within a {@link URIish} that a transport uses. */ - public static enum URIishField { + public enum URIishField { /** the user field */ USER, /** the pass (aka password) field */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java index 7ca9cc134c..d5e03ebd57 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java @@ -351,10 +351,7 @@ public class URIish implements Serializable { } private String n2e(String s) { - if (s == null) - return ""; //$NON-NLS-1$ - else - return s; + return s == null ? "" : s; //$NON-NLS-1$ } // takes care to cut of a leading slash if a windows drive letter or a diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java index 20b45882df..8a88e175fe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -44,6 +44,7 @@ package org.eclipse.jgit.transport; import static java.util.Collections.unmodifiableMap; +import static java.util.Objects.requireNonNull; import static org.eclipse.jgit.lib.Constants.R_TAGS; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_REF_IN_WANT; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SERVER_OPTION; @@ -84,7 +85,9 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; @@ -134,7 +137,7 @@ import org.eclipse.jgit.util.io.TimeoutOutputStream; */ public class UploadPack { /** Policy the server uses to validate client requests */ - public static enum RequestPolicy { + public enum RequestPolicy { /** Client may only ask for objects the server advertised a reference for. */ ADVERTISED, @@ -282,6 +285,8 @@ public class UploadPack { private OutputStream msgOut = NullOutputStream.INSTANCE; + private ErrorWriter errOut = new PackProtocolErrorWriter(); + /** * Refs eligible for advertising to the client, set using * {@link #setAdvertisedRefs}. @@ -756,9 +761,59 @@ public class UploadPack { /** * Execute the upload task on the socket. * - * <p>If the client passed extra parameters (e.g., "version=2") through a - * side channel, the caller must call setExtraParameters first to supply - * them. + * <p> + * Same as {@link #uploadWithExceptionPropagation} except that the thrown + * exceptions are handled in the method, and the error messages are sent to + * the clients. + * + * <p> + * Call this method if the caller does not have an error handling mechanism. + * Call {@link #uploadWithExceptionPropagation} if the caller wants to have + * its own error handling mechanism. + * + * @param input + * @param output + * @param messages + * @throws java.io.IOException + */ + public void upload(InputStream input, OutputStream output, + @Nullable OutputStream messages) throws IOException { + try { + uploadWithExceptionPropagation(input, output, messages); + } catch (ServiceMayNotContinueException err) { + if (!err.isOutput() && err.getMessage() != null) { + try { + errOut.writeError(err.getMessage()); + } catch (IOException e) { + err.addSuppressed(e); + throw err; + } + err.setOutput(); + } + throw err; + } catch (IOException | RuntimeException | Error err) { + if (rawOut != null) { + String msg = err instanceof PackProtocolException + ? err.getMessage() + : JGitText.get().internalServerError; + try { + errOut.writeError(msg); + } catch (IOException e) { + err.addSuppressed(e); + throw err; + } + throw new UploadPackInternalServerErrorException(err); + } + throw err; + } + } + + /** + * Execute the upload task on the socket. + * + * <p> + * If the client passed extra parameters (e.g., "version=2") through a side + * channel, the caller must call setExtraParameters first to supply them. * * @param input * raw input to read client commands from. Caller must ensure the @@ -772,15 +827,21 @@ public class UploadPack { * through. When run over SSH this should be tied back to the * standard error channel of the command execution. For most * other network connections this should be null. - * @throws java.io.IOException + * @throws ServiceMayNotContinueException + * thrown if one of the hooks throws this. + * @throws IOException + * thrown if the server or the client I/O fails, or there's an + * internal server error. + * @since 5.6 */ - public void upload(InputStream input, OutputStream output, - @Nullable OutputStream messages) throws IOException { - PacketLineOut pckOut = null; + public void uploadWithExceptionPropagation(InputStream input, + OutputStream output, @Nullable OutputStream messages) + throws ServiceMayNotContinueException, IOException { try { rawIn = input; - if (messages != null) + if (messages != null) { msgOut = messages; + } if (timeout > 0) { final Thread caller = Thread.currentThread(); @@ -800,42 +861,12 @@ public class UploadPack { } pckIn = new PacketLineIn(rawIn); - pckOut = new PacketLineOut(rawOut); + PacketLineOut pckOut = new PacketLineOut(rawOut); if (useProtocolV2()) { serviceV2(pckOut); } else { service(pckOut); } - } catch (UploadPackInternalServerErrorException err) { - // UploadPackInternalServerErrorException is a special exception - // that indicates an error is already written to the client. Do - // nothing. - throw err; - } catch (ServiceMayNotContinueException err) { - if (!err.isOutput() && err.getMessage() != null && pckOut != null) { - try { - pckOut.writeString("ERR " + err.getMessage() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ - } catch (IOException e) { - err.addSuppressed(e); - throw err; - } - err.setOutput(); - } - throw err; - } catch (IOException | RuntimeException | Error err) { - if (pckOut != null) { - String msg = err instanceof PackProtocolException - ? err.getMessage() - : JGitText.get().internalServerError; - try { - pckOut.writeString("ERR " + msg + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ - } catch (IOException e) { - err.addSuppressed(e); - throw err; - } - throw new UploadPackInternalServerErrorException(err); - } - throw err; } finally { msgOut = NullOutputStream.INSTANCE; walk.close(); @@ -1296,7 +1327,7 @@ public class UploadPack { caps.add(COMMAND_FETCH + '=' + (transferConfig.isAllowFilter() ? OPTION_FILTER + ' ' : "") + (advertiseRefInWant ? CAPABILITY_REF_IN_WANT + ' ' : "") - + (transferConfig.isAllowSidebandAll() + + (transferConfig.isAdvertiseSidebandAll() ? OPTION_SIDEBAND_ALL + ' ' : "") + (cachedPackUriProvider != null ? "packfile-uris " : "") @@ -1844,8 +1875,7 @@ public class UploadPack { @Override public void checkWants(UploadPack up, List<ObjectId> wants) throws PackProtocolException, IOException { - checkNotAdvertisedWants(up, wants, - refIdSet(up.getAdvertisedRefs().values())); + checkNotAdvertisedWants(up, wants, up.getAdvertisedRefs().values()); } } @@ -1882,7 +1912,7 @@ public class UploadPack { public void checkWants(UploadPack up, List<ObjectId> wants) throws PackProtocolException, IOException { checkNotAdvertisedWants(up, wants, - refIdSet(up.getRepository().getRefDatabase().getRefs())); + up.getRepository().getRefDatabase().getRefs()); } } @@ -1942,12 +1972,14 @@ public class UploadPack { } private static void checkNotAdvertisedWants(UploadPack up, - List<ObjectId> notAdvertisedWants, Set<ObjectId> reachableFrom) + List<ObjectId> notAdvertisedWants, Collection<Ref> visibleRefs) throws IOException { ObjectReader reader = up.getRevWalk().getObjectReader(); + try (RevWalk walk = new RevWalk(reader)) { walk.setRetainBody(false); + Set<ObjectId> reachableFrom = refIdSet(visibleRefs); // Missing "wants" throw exception here List<RevObject> wantsAsObjs = objectIdsToRevObjects(walk, notAdvertisedWants); @@ -1994,10 +2026,13 @@ public class UploadPack { ReachabilityChecker reachabilityChecker = walk .createReachabilityChecker(); - List<RevCommit> starters = objectIdsToRevCommits(walk, - reachableFrom); + Stream<RevCommit> reachableCommits = importantRefsFirst(visibleRefs) + .map(UploadPack::refToObjectId) + .map(objId -> objectIdToRevCommit(walk, objId)) + .filter(Objects::nonNull); // Ignore missing tips + Optional<RevCommit> unreachable = reachabilityChecker - .areAllReachable(wantsAsCommits, starters); + .areAllReachable(wantsAsCommits, reachableCommits); if (unreachable.isPresent()) { throw new WantNotValidException(unreachable.get()); } @@ -2007,6 +2042,50 @@ public class UploadPack { } } + static Stream<Ref> importantRefsFirst( + Collection<Ref> visibleRefs) { + Predicate<Ref> startsWithRefsHeads = ref -> ref.getName() + .startsWith(Constants.R_HEADS); + Predicate<Ref> startsWithRefsTags = ref -> ref.getName() + .startsWith(Constants.R_TAGS); + Predicate<Ref> allOther = ref -> !startsWithRefsHeads.test(ref) + && !startsWithRefsTags.test(ref); + + return Stream.concat( + visibleRefs.stream().filter(startsWithRefsHeads), + Stream.concat( + visibleRefs.stream().filter(startsWithRefsTags), + visibleRefs.stream().filter(allOther))); + } + + private static ObjectId refToObjectId(Ref ref) { + return ref.getObjectId() != null ? ref.getObjectId() + : ref.getPeeledObjectId(); + } + + /** + * Translate an object id to a RevCommit. + * + * @param walk + * walk on the relevant object storae + * @param objectId + * Object Id + * @return RevCommit instance or null if the object is missing + */ + @Nullable + private static RevCommit objectIdToRevCommit(RevWalk walk, + ObjectId objectId) { + if (objectId == null) { + return null; + } + + try { + return walk.parseCommit(objectId); + } catch (IOException e) { + return null; + } + } + // Resolve the ObjectIds into RevObjects. Any missing object raises an // exception private static List<RevObject> objectIdsToRevObjects(RevWalk walk, @@ -2019,23 +2098,6 @@ public class UploadPack { return result; } - // Get commits from object ids. If the id is not a commit, ignore it. If the - // id doesn't exist, report the missing object in a exception. - private static List<RevCommit> objectIdsToRevCommits(RevWalk walk, - Iterable<ObjectId> objectIds) - throws MissingObjectException, IOException { - List<RevCommit> result = new ArrayList<>(); - for (ObjectId objectId : objectIds) { - try { - result.add(walk.parseCommit(objectId)); - } catch (IncorrectObjectTypeException e) { - continue; - } - } - return result; - } - - private void addCommonBase(RevObject o) { if (!o.has(COMMON)) { o.add(COMMON); @@ -2114,54 +2176,42 @@ public class UploadPack { Set<String> caps = req.getClientCapabilities(); boolean sideband = caps.contains(OPTION_SIDE_BAND) || caps.contains(OPTION_SIDE_BAND_64K); + if (sideband) { - try { - sendPack(true, req, accumulator, allTags, unshallowCommits, - deepenNots, pckOut); - } catch (ServiceMayNotContinueException err) { - String message = err.getMessage(); - if (message == null) { - message = JGitText.get().internalServerError; - } - try { - reportInternalServerErrorOverSideband(message); - } catch (IOException e) { - err.addSuppressed(e); - throw err; - } - throw new UploadPackInternalServerErrorException(err); - } catch (IOException | RuntimeException | Error err) { - try { - reportInternalServerErrorOverSideband( - JGitText.get().internalServerError); - } catch (IOException e) { - err.addSuppressed(e); - throw err; - } - throw new UploadPackInternalServerErrorException(err); + errOut = new SideBandErrorWriter(); + + int bufsz = SideBandOutputStream.SMALL_BUF; + if (req.getClientCapabilities().contains(OPTION_SIDE_BAND_64K)) { + bufsz = SideBandOutputStream.MAX_BUF; } + OutputStream packOut = new SideBandOutputStream( + SideBandOutputStream.CH_DATA, bufsz, rawOut); + + ProgressMonitor pm = NullProgressMonitor.INSTANCE; + if (!req.getClientCapabilities().contains(OPTION_NO_PROGRESS)) { + msgOut = new SideBandOutputStream( + SideBandOutputStream.CH_PROGRESS, bufsz, rawOut); + pm = new SideBandProgressMonitor(msgOut); + } + + sendPack(pm, pckOut, packOut, req, accumulator, allTags, + unshallowCommits, deepenNots); + pckOut.end(); } else { - sendPack(false, req, accumulator, allTags, unshallowCommits, deepenNots, - pckOut); + sendPack(NullProgressMonitor.INSTANCE, pckOut, rawOut, req, + accumulator, allTags, unshallowCommits, deepenNots); } } - private void reportInternalServerErrorOverSideband(String message) - throws IOException { - @SuppressWarnings("resource" /* java 7 */) - SideBandOutputStream err = new SideBandOutputStream( - SideBandOutputStream.CH_ERROR, SideBandOutputStream.SMALL_BUF, - rawOut); - err.write(Constants.encode(message)); - err.flush(); - } - /** * Send the requested objects to the client. * - * @param sideband - * whether to wrap the pack in side-band pkt-lines, interleaved - * with progress messages and errors. + * @param pm + * progress monitor + * @param pckOut + * PacketLineOut that shares the output with packOut + * @param packOut + * packfile output * @param req * request being processed * @param accumulator @@ -2173,35 +2223,14 @@ public class UploadPack { * shallow commits on the client that are now becoming unshallow * @param deepenNots * objects that the client specified using --shallow-exclude - * @param pckOut - * output writer * @throws IOException * if an error occurred while generating or writing the pack. */ - private void sendPack(final boolean sideband, - FetchRequest req, + private void sendPack(ProgressMonitor pm, PacketLineOut pckOut, + OutputStream packOut, FetchRequest req, PackStatistics.Accumulator accumulator, - @Nullable Collection<Ref> allTags, - List<ObjectId> unshallowCommits, - List<ObjectId> deepenNots, - PacketLineOut pckOut) throws IOException { - ProgressMonitor pm = NullProgressMonitor.INSTANCE; - OutputStream packOut = rawOut; - - if (sideband) { - int bufsz = SideBandOutputStream.SMALL_BUF; - if (req.getClientCapabilities().contains(OPTION_SIDE_BAND_64K)) - bufsz = SideBandOutputStream.MAX_BUF; - - packOut = new SideBandOutputStream(SideBandOutputStream.CH_DATA, - bufsz, rawOut); - if (!req.getClientCapabilities().contains(OPTION_NO_PROGRESS)) { - msgOut = new SideBandOutputStream( - SideBandOutputStream.CH_PROGRESS, bufsz, rawOut); - pm = new SideBandProgressMonitor(msgOut); - } - } - + @Nullable Collection<Ref> allTags, List<ObjectId> unshallowCommits, + List<ObjectId> deepenNots) throws IOException { if (wantAll.isEmpty()) { preUploadHook.onSendPack(this, wantIds, commonBase); } else { @@ -2344,9 +2373,6 @@ public class UploadPack { } pw.close(); } - - if (sideband) - pckOut.end(); } private static void findSymrefs( @@ -2385,12 +2411,12 @@ public class UploadPack { } @Override - public void write(byte b[]) throws IOException { + public void write(byte[] b) throws IOException { out.write(b); } @Override - public void write(byte b[], int off, int len) throws IOException { + public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); } @@ -2411,4 +2437,28 @@ public class UploadPack { } } } + + private interface ErrorWriter { + void writeError(String message) throws IOException; + } + + private class SideBandErrorWriter implements ErrorWriter { + @Override + public void writeError(String message) throws IOException { + @SuppressWarnings("resource" /* java 7 */) + SideBandOutputStream err = new SideBandOutputStream( + SideBandOutputStream.CH_ERROR, + SideBandOutputStream.SMALL_BUF, requireNonNull(rawOut)); + err.write(Constants.encode(message)); + err.flush(); + } + } + + private class PackProtocolErrorWriter implements ErrorWriter { + @Override + public void writeError(String message) throws IOException { + new PacketLineOut(requireNonNull(rawOut)) + .writeString("ERR " + message + '\n'); //$NON-NLS-1$ + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java index 7a973aff34..fb8a55f1b5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkEncryption.java @@ -324,7 +324,7 @@ abstract class WalkEncryption { * Base implementation of JGit symmetric encryption. Supports V2 properties * format. */ - static abstract class SymmetricEncryption extends WalkEncryption + abstract static class SymmetricEncryption extends WalkEncryption implements Keys, Vals { /** Encryption profile, root name of group of related properties. */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java index b4a7af094d..9a08c080c3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java @@ -124,8 +124,8 @@ public class FileResolver<C> implements RepositoryResolver<C> { // are responsible for closing the repository if we // complete successfully. return db; - } else - throw new ServiceNotEnabledException(); + } + throw new ServiceNotEnabledException(); } catch (RuntimeException | IOException e) { db.close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java index 4f3eb05b11..5f5da58771 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java @@ -275,7 +275,7 @@ public class FileTreeIterator extends WorkingTreeIterator { /** * a singleton instance of the default FileModeStrategy */ - public final static DefaultFileModeStrategy INSTANCE = + public static final DefaultFileModeStrategy INSTANCE = new DefaultFileModeStrategy(); @Override @@ -285,9 +285,8 @@ public class FileTreeIterator extends WorkingTreeIterator { } else if (attributes.isDirectory()) { if (new File(f, Constants.DOT_GIT).exists()) { return FileMode.GITLINK; - } else { - return FileMode.TREE; } + return FileMode.TREE; } else if (attributes.isExecutable()) { return FileMode.EXECUTABLE_FILE; } else { @@ -309,7 +308,7 @@ public class FileTreeIterator extends WorkingTreeIterator { /** * a singleton instance of the default FileModeStrategy */ - public final static NoGitlinksStrategy INSTANCE = new NoGitlinksStrategy(); + public static final NoGitlinksStrategy INSTANCE = new NoGitlinksStrategy(); @Override public FileMode getMode(File f, FS.Attributes attributes) { @@ -425,9 +424,8 @@ public class FileTreeIterator extends WorkingTreeIterator { if (attributes.isSymbolicLink()) { return new ByteArrayInputStream(fs.readSymLink(getFile()) .getBytes(UTF_8)); - } else { - return new FileInputStream(getFile()); } + return new FileInputStream(getFile()); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java index 65d8512179..e545565e8f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -111,7 +111,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { /** * @since 4.2 */ - public static enum OperationType { + public enum OperationType { /** * Represents a checkout operation (for example a checkout or reset * operation). diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java index 8bb68dca17..35d6e41a9e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -101,7 +101,6 @@ import org.eclipse.jgit.util.Paths; import org.eclipse.jgit.util.RawParseUtils; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer.LocalFile; -import org.eclipse.jgit.util.io.AutoLFInputStream; import org.eclipse.jgit.util.io.EolStreamTypeUtil; import org.eclipse.jgit.util.sha1.SHA1; @@ -686,10 +685,10 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { public InputStream openEntryStream() throws IOException { InputStream rawis = current().openInputStream(); if (getCleanFilterCommand() == null - && getEolStreamType() == EolStreamType.DIRECT) + && getEolStreamType() == EolStreamType.DIRECT) { return rawis; - else - return filterClean(rawis); + } + return filterClean(rawis); } /** @@ -980,13 +979,13 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { MetadataDiff diff = compareMetadata(entry); switch (diff) { case DIFFER_BY_TIMESTAMP: - if (forceContentCheck) + if (forceContentCheck) { // But we are told to look at content even though timestamps // tell us about modification return contentCheck(entry, reader); - else - // We are told to assume a modification if timestamps differs - return true; + } + // We are told to assume a modification if timestamps differs + return true; case SMUDGED: // The file is clean by timestamps but the entry was smudged. // Lets do a content check @@ -1087,47 +1086,13 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { entry.setLength((int) getEntryLength()); return false; - } else { - if (mode == FileMode.SYMLINK.getBits()) { - return !new File(readSymlinkTarget(current())).equals( - new File(readContentAsNormalizedString(entry, reader))); - } - // Content differs: that's a real change, perhaps - if (reader == null) // deprecated use, do no further checks - return true; - - switch (getEolStreamType()) { - case DIRECT: - return true; - default: - try { - ObjectLoader loader = reader.open(entry.getObjectId()); - if (loader == null) - return true; - - // We need to compute the length, but only if it is not - // a binary stream. - long dcInLen; - try (InputStream dcIn = new AutoLFInputStream( - loader.openStream(), true, - true /* abort if binary */)) { - dcInLen = computeLength(dcIn); - } catch (AutoLFInputStream.IsBinaryException e) { - return true; - } - - try (InputStream dcIn = new AutoLFInputStream( - loader.openStream(), true)) { - byte[] autoCrLfHash = computeHash(dcIn, dcInLen); - boolean changed = getEntryObjectId() - .compareTo(autoCrLfHash, 0) != 0; - return changed; - } - } catch (IOException e) { - return true; - } - } } + if (mode == FileMode.SYMLINK.getBits()) { + return !new File(readSymlinkTarget(current())).equals( + new File(readContentAsNormalizedString(entry, reader))); + } + // Content differs: that's a real change + return true; } private static String readContentAsNormalizedString(DirCacheEntry entry, @@ -1215,7 +1180,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { * * @since 5.0 */ - public static abstract class Entry { + public abstract static class Entry { byte[] encodedName; int encodedNameLen; @@ -1535,7 +1500,7 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } // Read blob from index and check for CR/LF-delimited text. DirCacheEntry entry = dirCache.getDirCacheEntry(); - if (FileMode.REGULAR_FILE.equals(entry.getFileMode())) { + if ((entry.getRawMode() & FileMode.TYPE_MASK) == FileMode.TYPE_FILE) { ObjectId blobId = entry.getObjectId(); if (entry.getStage() > 0 && entry.getStage() != DirCacheEntry.STAGE_2) { @@ -1552,7 +1517,10 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { break; } if (entry.getStage() == DirCacheEntry.STAGE_2) { - blobId = entry.getObjectId(); + if ((entry.getRawMode() + & FileMode.TYPE_MASK) == FileMode.TYPE_FILE) { + blobId = entry.getObjectId(); + } break; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/IndexDiffFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/IndexDiffFilter.java index 6cca582b3b..52fb888291 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/IndexDiffFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/IndexDiffFilter.java @@ -212,10 +212,9 @@ public class IndexDiffFilter extends TreeFilter { // If i is cnt then the path does not appear in any other tree, // and this working tree entry can be safely ignored. return i != cnt; - } else { - // In working tree and not ignored, and not in DirCache. - return true; } + // In working tree and not ignored, and not in DirCache. + return true; } // Always include subtrees as WorkingTreeIterator cannot provide diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathSuffixFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathSuffixFilter.java index 3d9f875e99..11896e4242 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathSuffixFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathSuffixFilter.java @@ -99,10 +99,10 @@ public class PathSuffixFilter extends TreeFilter { @Override public boolean include(TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, IOException { - if (walker.isSubtree()) + if (walker.isSubtree()) { return true; - else - return walker.isPathSuffix(pathRaw, pathRaw.length); + } + return walker.isPathSuffix(pathRaw, pathRaw.length); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java index 69f8547452..119c96e02e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Base64.java @@ -28,26 +28,26 @@ import org.eclipse.jgit.internal.JGitText; */ public class Base64 { /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte) '='; + private static final byte EQUALS_SIGN = (byte) '='; /** Indicates equals sign in encoding. */ - private final static byte EQUALS_SIGN_DEC = -1; + private static final byte EQUALS_SIGN_DEC = -1; /** Indicates white space in encoding. */ - private final static byte WHITE_SPACE_DEC = -2; + private static final byte WHITE_SPACE_DEC = -2; /** Indicates an invalid byte during decoding. */ - private final static byte INVALID_DEC = -3; + private static final byte INVALID_DEC = -3; /** The 64 valid Base64 values. */ - private final static byte[] ENC; + private static final byte[] ENC; /** * Translates a Base64 value to either its 6-bit reconstruction value or a * negative number indicating some other meaning. The table is only 7 bits * wide, as the 8th bit is discarded during decoding. */ - private final static byte[] DEC; + private static final byte[] DEC; static { ENC = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ" // //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java index c8e6645f57..957cb5af6d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java @@ -173,20 +173,19 @@ public class ChangeIdUtil { boolean replaceExisting) { int indexOfChangeId = indexOfChangeId(message, "\n"); //$NON-NLS-1$ if (indexOfChangeId > 0) { - if (!replaceExisting) + if (!replaceExisting) { return message; - else { - StringBuilder ret = new StringBuilder(message.substring(0, - indexOfChangeId)); - ret.append(CHANGE_ID); - ret.append(" I"); //$NON-NLS-1$ - ret.append(ObjectId.toString(changeId)); - int indexOfNextLineBreak = message.indexOf("\n", //$NON-NLS-1$ - indexOfChangeId); - if (indexOfNextLineBreak > 0) - ret.append(message.substring(indexOfNextLineBreak)); - return ret.toString(); } + StringBuilder ret = new StringBuilder( + message.substring(0, indexOfChangeId)); + ret.append(CHANGE_ID); + ret.append(" I"); //$NON-NLS-1$ + ret.append(ObjectId.toString(changeId)); + int indexOfNextLineBreak = message.indexOf('\n', + indexOfChangeId); + if (indexOfNextLineBreak > 0) + ret.append(message.substring(indexOfNextLineBreak)); + return ret.toString(); } String[] lines = message.split("\n"); //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java index 0aa0e3668d..41bfde9914 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -61,6 +61,7 @@ import java.nio.charset.Charset; import java.nio.file.AccessDeniedException; import java.nio.file.FileStore; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; @@ -98,6 +99,7 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.LockFailedException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.file.FileSnapshot; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; @@ -147,15 +149,15 @@ public abstract class FS { */ public FS detect(Boolean cygwinUsed) { if (SystemReader.getInstance().isWindows()) { - if (cygwinUsed == null) + if (cygwinUsed == null) { cygwinUsed = Boolean.valueOf(FS_Win32_Cygwin.isCygwin()); - if (cygwinUsed.booleanValue()) + } + if (cygwinUsed.booleanValue()) { return new FS_Win32_Cygwin(); - else - return new FS_Win32(); - } else { - return new FS_POSIX(); + } + return new FS_Win32(); } + return new FS_POSIX(); } } @@ -211,7 +213,7 @@ public abstract class FS { * * @since 5.1.9 */ - public final static class FileStoreAttributes { + public static final class FileStoreAttributes { private static final Duration UNDEFINED_DURATION = Duration .ofNanos(Long.MAX_VALUE); @@ -240,13 +242,21 @@ public abstract class FS { private static Map<FileStore, Lock> locks = new ConcurrentHashMap<>(); - private static void setBackground(boolean async) { + /** + * Whether FileStore attributes should be determined asynchronously + * + * @param async + * whether FileStore attributes should be determined + * asynchronously. If false access to cached attributes may block + * for some seconds for the first call per FileStore + * @since 5.6.2 + */ + public static void setBackground(boolean async) { background.set(async); } - private static final String javaVersionPrefix = SystemReader - .getInstance().getHostname() + '|' - + System.getProperty("java.vendor") + '|' //$NON-NLS-1$ + private static final String javaVersionPrefix = System + .getProperty("java.vendor") + '|' //$NON-NLS-1$ + System.getProperty("java.version") + '|'; //$NON-NLS-1$ private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration @@ -689,7 +699,7 @@ public abstract class FS { /** The auto-detected implementation selected for this operating system and JRE. */ public static final FS DETECTED = detect(); - private volatile static FSFactory factory; + private static volatile FSFactory factory; /** * Auto-detect the appropriate file system abstraction. @@ -708,7 +718,9 @@ public abstract class FS { * asynchronously. If false access to cached attributes may block * for some seconds for the first call per FileStore * @since 5.1.9 + * @deprecated Use {@link FileStoreAttributes#setBackground} instead */ + @Deprecated public static void setAsyncFileStoreAttributes(boolean asynch) { FileStoreAttributes.setBackground(asynch); } @@ -837,7 +849,7 @@ public abstract class FS { try { FileUtils.delete(tempFile); } catch (IOException e) { - throw new RuntimeException(e); // panic + LOG.error(JGitText.get().cannotDeleteFile, tempFile); } } } @@ -1192,14 +1204,13 @@ public abstract class FS { gobbler.join(); if (rc == 0 && !gobbler.fail.get()) { return r; - } else { - if (debug) { - LOG.debug("readpipe rc=" + rc); //$NON-NLS-1$ - } - throw new CommandFailedException(rc, - gobbler.errorMessage.get(), - gobbler.exception.get()); } + if (debug) { + LOG.debug("readpipe rc=" + rc); //$NON-NLS-1$ + } + throw new CommandFailedException(rc, + gobbler.errorMessage.get(), + gobbler.exception.get()); } catch (InterruptedException ie) { // Stop bothering me, I have a zombie to reap. } @@ -1726,20 +1737,18 @@ public abstract class FS { final String hookName, String[] args, PrintStream outRedirect, PrintStream errRedirect, String stdinArgs) throws JGitInternalException { - final File hookFile = findHook(repository, hookName); - if (hookFile == null) + File hookFile = findHook(repository, hookName); + if (hookFile == null || hookName == null) { return new ProcessResult(Status.NOT_PRESENT); + } - final String hookPath = hookFile.getAbsolutePath(); - final File runDirectory; - if (repository.isBare()) - runDirectory = repository.getDirectory(); - else - runDirectory = repository.getWorkTree(); - final String cmd = relativize(runDirectory.getAbsolutePath(), - hookPath); - ProcessBuilder hookProcess = runInShell(cmd, args); - hookProcess.directory(runDirectory); + File runDirectory = getRunDirectory(repository, hookName); + if (runDirectory == null) { + return new ProcessResult(Status.NOT_PRESENT); + } + String cmd = hookFile.getAbsolutePath(); + ProcessBuilder hookProcess = runInShell(shellQuote(cmd), args); + hookProcess.directory(runDirectory.getAbsoluteFile()); Map<String, String> environment = hookProcess.environment(); environment.put(Constants.GIT_DIR_KEY, repository.getDirectory().getAbsolutePath()); @@ -1761,6 +1770,21 @@ public abstract class FS { } } + /** + * Quote a string (such as a file system path obtained from a Java + * {@link File} or {@link Path} object) such that it can be passed as first + * argument to {@link #runInShell(String, String[])}. + * <p> + * This default implementation returns the string unchanged. + * </p> + * + * @param cmd + * the String to quote + * @return the quoted string + */ + String shellQuote(String cmd) { + return cmd; + } /** * Tries to find a hook matching the given one in the given repository. @@ -1774,12 +1798,71 @@ public abstract class FS { * @since 4.0 */ public File findHook(Repository repository, String hookName) { - File gitDir = repository.getDirectory(); - if (gitDir == null) + if (hookName == null) { + return null; + } + File hookDir = getHooksDirectory(repository); + if (hookDir == null) { return null; - final File hookFile = new File(new File(gitDir, - Constants.HOOKS), hookName); - return hookFile.isFile() ? hookFile : null; + } + File hookFile = new File(hookDir, hookName); + if (hookFile.isAbsolute()) { + if (!hookFile.exists() || (FS.DETECTED.supportsExecute() + && !FS.DETECTED.canExecute(hookFile))) { + return null; + } + } else { + try { + File runDirectory = getRunDirectory(repository, hookName); + if (runDirectory == null) { + return null; + } + Path hookPath = runDirectory.getAbsoluteFile().toPath() + .resolve(hookFile.toPath()); + FS fs = repository.getFS(); + if (fs == null) { + fs = FS.DETECTED; + } + if (!Files.exists(hookPath) || (fs.supportsExecute() + && !fs.canExecute(hookPath.toFile()))) { + return null; + } + hookFile = hookPath.toFile(); + } catch (InvalidPathException e) { + LOG.warn(MessageFormat.format(JGitText.get().invalidHooksPath, + hookFile)); + return null; + } + } + return hookFile; + } + + private File getRunDirectory(Repository repository, + @NonNull String hookName) { + if (repository.isBare()) { + return repository.getDirectory(); + } + switch (hookName) { + case "pre-receive": //$NON-NLS-1$ + case "update": //$NON-NLS-1$ + case "post-receive": //$NON-NLS-1$ + case "post-update": //$NON-NLS-1$ + case "push-to-checkout": //$NON-NLS-1$ + return repository.getDirectory(); + default: + return repository.getWorkTree(); + } + } + + private File getHooksDirectory(Repository repository) { + Config config = repository.getConfig(); + String hooksDir = config.getString(ConfigConstants.CONFIG_CORE_SECTION, + null, ConfigConstants.CONFIG_KEY_HOOKS_PATH); + if (hooksDir != null) { + return new File(hooksDir); + } + File dir = repository.getDirectory(); + return dir == null ? null : new File(dir, Constants.HOOKS); } /** @@ -2213,7 +2296,7 @@ public abstract class FS { void copy() throws IOException { boolean writeFailure = false; - byte buffer[] = new byte[4096]; + byte[] buffer = new byte[4096]; int readBytes; while ((readBytes = in.read(buffer)) != -1) { // Do not try to write again after a failure, but keep diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java index 6a1eef2d66..9c8dab68cd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java @@ -74,7 +74,6 @@ import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.errors.CommandFailedException; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.slf4j.Logger; @@ -86,7 +85,7 @@ import org.slf4j.LoggerFactory; * @since 3.0 */ public class FS_POSIX extends FS { - private final static Logger LOG = LoggerFactory.getLogger(FS_POSIX.class); + private static final Logger LOG = LoggerFactory.getLogger(FS_POSIX.class); private static final int DEFAULT_UMASK = 0022; private volatile int umask = -1; @@ -270,6 +269,11 @@ public class FS_POSIX extends FS { return proc; } + @Override + String shellQuote(String cmd) { + return QuotedString.BOURNE.quote(cmd); + } + /** {@inheritDoc} */ @Override public ProcessResult runHookIfPresent(Repository repository, String hookName, @@ -311,20 +315,6 @@ public class FS_POSIX extends FS { /** {@inheritDoc} */ @Override - public File findHook(Repository repository, String hookName) { - final File gitdir = repository.getDirectory(); - if (gitdir == null) { - return null; - } - final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS) - .resolve(hookName); - if (Files.isExecutable(hookPath)) - return hookPath.toFile(); - return null; - } - - /** {@inheritDoc} */ - @Override public boolean supportsAtomicCreateNewFile() { if (supportsAtomicFileCreation == AtomicFileCreation.UNDEFINED) { try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java index 1e64a38bb1..aedf43c9e1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java @@ -72,7 +72,7 @@ import org.slf4j.LoggerFactory; * @since 3.0 */ public class FS_Win32 extends FS { - private final static Logger LOG = LoggerFactory.getLogger(FS_Win32.class); + private static final Logger LOG = LoggerFactory.getLogger(FS_Win32.class); /** * Constructor diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java index 9a163e8e38..ac788a6684 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java @@ -47,8 +47,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.File; import java.io.PrintStream; -import java.nio.file.Files; -import java.nio.file.Path; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; @@ -57,7 +55,6 @@ import java.util.List; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.errors.CommandFailedException; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,7 +65,7 @@ import org.slf4j.LoggerFactory; * @since 3.0 */ public class FS_Win32_Cygwin extends FS_Win32 { - private final static Logger LOG = LoggerFactory + private static final Logger LOG = LoggerFactory .getLogger(FS_Win32_Cygwin.class); private static String cygpath; @@ -160,6 +157,11 @@ public class FS_Win32_Cygwin extends FS_Win32 { return proc; } + @Override + String shellQuote(String cmd) { + return QuotedString.BOURNE.quote(cmd.replace(File.separatorChar, '/')); + } + /** {@inheritDoc} */ @Override public String relativize(String base, String other) { @@ -175,18 +177,4 @@ public class FS_Win32_Cygwin extends FS_Win32 { return internalRunHookIfPresent(repository, hookName, args, outRedirect, errRedirect, stdinArgs); } - - /** {@inheritDoc} */ - @Override - public File findHook(Repository repository, String hookName) { - final File gitdir = repository.getDirectory(); - if (gitdir == null) { - return null; - } - final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS) - .resolve(hookName); - if (Files.isExecutable(hookPath)) - return hookPath.toFile(); - return null; - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java index 4d791e470a..e026e9274f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java @@ -73,6 +73,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Random; import java.util.regex.Pattern; import org.eclipse.jgit.internal.JGitText; @@ -87,6 +88,8 @@ import org.slf4j.LoggerFactory; public class FileUtils { private static final Logger LOG = LoggerFactory.getLogger(FileUtils.class); + private static final Random RNG = new Random(); + /** * Option to delete given {@code File} */ @@ -986,4 +989,28 @@ public class FileUtils { } Files.setLastModifiedTime(f, FileTime.from(Instant.now())); } + + /** + * Compute a delay in a {@code min..max} interval with random jitter. + * + * @param last + * amount of delay waited before the last attempt. This is used + * to seed the next delay interval. Should be 0 if there was no + * prior delay. + * @param min + * shortest amount of allowable delay between attempts. + * @param max + * longest amount of allowable delay between attempts. + * @return new amount of delay to wait before the next attempt. + * + * @since 5.6 + */ + public static long delay(long last, long min, long max) { + long r = Math.max(0, last * 3 - min); + if (r > 0) { + int c = (int) Math.min(r + 1, Integer.MAX_VALUE); + r = RNG.nextInt(c); + } + return Math.max(Math.min(min + r, max), min); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java index e461902a31..e9f65d2cdc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateFormatter.java @@ -68,7 +68,7 @@ public class GitDateFormatter { /** * Git and JGit formats */ - static public enum Format { + public enum Format { /** * Git format: Time and original time zone diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java index 56a173163d..c6a6899948 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/GitDateParser.java @@ -144,7 +144,7 @@ public class GitDateParser { * <li>"yesterday"</li> * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br> * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of ' - * ' one can use '.' to seperate the words</li> + * ' one can use '.' to separate the words</li> * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li> * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li> * <li>"yyyy-MM-dd"</li> @@ -186,7 +186,7 @@ public class GitDateParser { * <li>"yesterday"</li> * <li>"(x) years|months|weeks|days|hours|minutes|seconds ago"<br> * Multiple specs can be combined like in "2 weeks 3 days ago". Instead of ' - * ' one can use '.' to seperate the words</li> + * ' one can use '.' to separate the words</li> * <li>"yyyy-MM-dd HH:mm:ss Z" (ISO)</li> * <li>"EEE, dd MMM yyyy HH:mm:ss Z" (RFC)</li> * <li>"yyyy-MM-dd"</li> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java index 640670debc..d897255062 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java @@ -51,6 +51,7 @@ import java.io.UnsupportedEncodingException; import java.net.ConnectException; import java.net.Proxy; import java.net.ProxySelector; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; @@ -299,7 +300,9 @@ public class HttpSupport { public static Proxy proxyFor(ProxySelector proxySelector, URL u) throws ConnectException { try { - return proxySelector.select(u.toURI()).get(0); + URI uri = new URI(u.getProtocol(), null, u.getHost(), u.getPort(), + null, null, null); + return proxySelector.select(uri).get(0); } catch (URISyntaxException e) { final ConnectException err; err = new ConnectException(MessageFormat.format(JGitText.get().cannotDetermineProxyFor, u)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java index a07a4fd1a5..391598d8ae 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java @@ -345,13 +345,14 @@ public class IO { c = s.charAt(++i); l.add(sb.toString()); sb.setLength(0); - if (c != '\n') + if (c != '\n') { sb.append(c); + } continue; - } else { // EOF - l.add(sb.toString()); - break; } + // EOF + l.add(sb.toString()); + break; } sb.append(c); } @@ -401,20 +402,18 @@ public class IO { } resetAndSkipFully(in, n); } - } else { - StringBuilder buf = sizeHint > 0 - ? new StringBuilder(sizeHint) - : new StringBuilder(); - int i; - while ((i = in.read()) != -1) { - char c = (char) i; - buf.append(c); - if (c == '\n') { - break; - } + } + StringBuilder buf = sizeHint > 0 ? new StringBuilder(sizeHint) + : new StringBuilder(); + int i; + while ((i = in.read()) != -1) { + char c = (char) i; + buf.append(c); + if (c == '\n') { + break; } - return buf.toString(); } + return buf.toString(); } private static void resetAndSkipFully(Reader fd, long toSkip) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java index 96636b7994..0f6620e815 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/LfsFactory.java @@ -145,7 +145,7 @@ public class LfsFactory { } /** - * Retrieve a pre-push hook to be applied. + * Retrieve a pre-push hook to be applied using the default error stream. * * @param repo * the {@link Repository} the hook is applied to. @@ -159,6 +159,22 @@ public class LfsFactory { } /** + * Retrieve a pre-push hook to be applied. + * + * @param repo + * the {@link Repository} the hook is applied to. + * @param outputStream + * @param errorStream + * @return a {@link PrePushHook} implementation or <code>null</code> + * @since 5.6 + */ + @Nullable + public PrePushHook getPrePushHook(Repository repo, PrintStream outputStream, + PrintStream errorStream) { + return getPrePushHook(repo, outputStream); + } + + /** * Retrieve an {@link LfsInstallCommand} which can be used to enable LFS * support (if available) either per repository or for the user. * @@ -297,7 +313,7 @@ public class LfsFactory { } @Override - public int read(byte b[], int off, int len) throws IOException { + public int read(byte[] b, int off, int len) throws IOException { return stream.read(b, off, len); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Monitoring.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Monitoring.java index 83bf695f70..500c236730 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/Monitoring.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Monitoring.java @@ -33,7 +33,7 @@ import org.slf4j.LoggerFactory; * @since 5.1.13 */ public class Monitoring { - private final static Logger LOG = LoggerFactory.getLogger(Monitoring.class); + private static final Logger LOG = LoggerFactory.getLogger(Monitoring.class); /** * Register a MBean with the platform MBean server @@ -49,7 +49,7 @@ public class Monitoring { String metricName) { boolean register = false; try { - Class<?> interfaces[] = mbean.getClass().getInterfaces(); + Class<?>[] interfaces = mbean.getClass().getInterfaces(); for (Class<?> i : interfaces) { register = SystemReader.getInstance().getUserConfig() .getBoolean( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java index 9267a325f4..1180d4c2ac 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java @@ -51,7 +51,7 @@ public class ProcessResult { /** * Status of a process' execution. */ - public static enum Status { + public enum Status { /** * The script was found and launched properly. It may still have exited * with a non-zero {@link #exitCode}. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/QuotedString.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/QuotedString.java index a55cad3705..2b2358abe9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/QuotedString.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/QuotedString.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008, Google Inc. + * Copyright (C) 2008, 2019 Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -54,7 +54,15 @@ import org.eclipse.jgit.lib.Constants; */ public abstract class QuotedString { /** Quoting style that obeys the rules Git applies to file names */ - public static final GitPathStyle GIT_PATH = new GitPathStyle(); + public static final GitPathStyle GIT_PATH = new GitPathStyle(true); + + /** + * Quoting style that obeys the rules Git applies to file names when + * {@code core.quotePath = false}. + * + * @since 5.6 + */ + public static final QuotedString GIT_PATH_MINIMAL = new GitPathStyle(false); /** * Quoting style used by the Bourne shell. @@ -256,40 +264,48 @@ public abstract class QuotedString { quote['"'] = '"'; } + private final boolean quoteHigh; + @Override public String quote(String instr) { - if (instr.length() == 0) + if (instr.isEmpty()) { return "\"\""; //$NON-NLS-1$ + } boolean reuse = true; final byte[] in = Constants.encode(instr); - final StringBuilder r = new StringBuilder(2 + in.length); - r.append('"'); + final byte[] out = new byte[4 * in.length + 2]; + int o = 0; + out[o++] = '"'; for (int i = 0; i < in.length; i++) { final int c = in[i] & 0xff; if (c < quote.length) { final byte style = quote[c]; if (style == 0) { - r.append((char) c); + out[o++] = (byte) c; continue; } if (style > 0) { reuse = false; - r.append('\\'); - r.append((char) style); + out[o++] = '\\'; + out[o++] = style; continue; } + } else if (!quoteHigh) { + out[o++] = (byte) c; + continue; } reuse = false; - r.append('\\'); - r.append((char) (((c >> 6) & 03) + '0')); - r.append((char) (((c >> 3) & 07) + '0')); - r.append((char) (((c >> 0) & 07) + '0')); + out[o++] = '\\'; + out[o++] = (byte) (((c >> 6) & 03) + '0'); + out[o++] = (byte) (((c >> 3) & 07) + '0'); + out[o++] = (byte) (((c >> 0) & 07) + '0'); } - if (reuse) + if (reuse) { return instr; - r.append('"'); - return r.toString(); + } + out[o++] = '"'; + return new String(out, 0, o, UTF_8); } @Override @@ -375,8 +391,8 @@ public abstract class QuotedString { return RawParseUtils.decode(UTF_8, r, 0, rPtr); } - private GitPathStyle() { - // Singleton + private GitPathStyle(boolean doQuote) { + quoteHigh = doQuote; } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java index 9663e3cef5..ce1308f334 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefMap.java @@ -191,12 +191,11 @@ public class RefMap extends AbstractMap<String, Ref> { Ref prior = loose.get(name); loose = loose.set(idx, value); return prior; - } else { - Ref prior = get(keyName); - loose = loose.add(idx, value); - sizeIsValid = false; - return prior; } + Ref prior = get(keyName); + loose = loose.add(idx, value); + sizeIsValid = false; + return prior; } /** {@inheritDoc} */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java index 83c60c63bc..55f39c265d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java @@ -52,19 +52,19 @@ import org.eclipse.jgit.internal.JGitText; * in the format defined by {@code git log --relative-date}. */ public class RelativeDateFormatter { - final static long SECOND_IN_MILLIS = 1000; + static final long SECOND_IN_MILLIS = 1000; - final static long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS; + static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS; - final static long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS; + static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS; - final static long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS; + static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS; - final static long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS; + static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS; - final static long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS; + static final long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS; - final static long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS; + static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS; /** * Get age of given {@link java.util.Date} compared to now formatted in the diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java index e16a88655c..87ce4752b7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java @@ -85,7 +85,7 @@ import org.slf4j.LoggerFactory; */ public abstract class SystemReader { - private final static Logger LOG = LoggerFactory + private static final Logger LOG = LoggerFactory .getLogger(SystemReader.class); private static final SystemReader DEFAULT; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/LimitedInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/LimitedInputStream.java index 9ab2caa1ac..e437c1114b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/LimitedInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/LimitedInputStream.java @@ -98,15 +98,16 @@ public abstract class LimitedInputStream extends FilterInputStream { @Override public int read() throws IOException { if (left == 0) { - if (in.available() == 0) + if (in.available() == 0) { return -1; - else - limitExceeded(); + } + limitExceeded(); } int result = in.read(); - if (result != -1) + if (result != -1) { --left; + } return result; } @@ -114,16 +115,17 @@ public abstract class LimitedInputStream extends FilterInputStream { @Override public int read(byte[] b, int off, int len) throws IOException { if (left == 0) { - if (in.available() == 0) + if (in.available() == 0) { return -1; - else - limitExceeded(); + } + limitExceeded(); } len = (int) Math.min(len, left); int result = in.read(b, off, len); - if (result != -1) + if (result != -1) { left -= result; + } return result; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1.java index 1ad6602fce..e6971ee205 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/sha1/SHA1.java @@ -78,7 +78,7 @@ import org.slf4j.LoggerFactory; * @since 4.7 */ public class SHA1 { - private static Logger LOG = LoggerFactory.getLogger(SHA1.class); + private static final Logger LOG = LoggerFactory.getLogger(SHA1.class); private static final boolean DETECT_COLLISIONS; static { |