# HG changeset patch # User Artem Tikhomirov # Date 1366396234 -7200 # Node ID bd5926e24aa3ef2cc989e77164003f090fff5a2c # Parent 36e36b92674770779c8f8db95d75c4197047be63 Respect unix flags for checkout/revert diff -r 36e36b926747 -r bd5926e24aa3 src/org/tmatesoft/hg/core/HgCheckoutCommand.java --- a/src/org/tmatesoft/hg/core/HgCheckoutCommand.java Wed Apr 17 16:06:10 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgCheckoutCommand.java Fri Apr 19 20:30:34 2013 +0200 @@ -135,8 +135,19 @@ public boolean next(Nodeid nid, Path fname, Flags flags) { if (worker.next(nid, fname, flags)) { - // new dirstate based on manifest - dirstateBuilder.recordNormal(fname, flags, worker.getLastWrittenFileSize()); + // Mercurial seems to write "n 0 -1 unset fname" on `hg --clean co -rev ` + // and the reason for 'force lookup' I suspect is a slight chance of simultaneous modification + // of the file by user that doesn't alter its size the very second dirstate is being written + // (or the file is being updated and the update brought in changes that didn't alter the file size - + // with size and timestamp set, later `hg status` won't notice these changes) + + // However, as long as we use this class to write clean copies of the files, we can put all the fields + // right away. + int mtime = worker.getLastFileModificationTime(); + // Manifest flags are chars (despite octal values `hg manifest --debug` displays), + // while dirstate keeps actual unix flags. + int fmode = worker.getLastFileMode(); + dirstateBuilder.recordNormal(fname, fmode, mtime, worker.getLastFileSize()); return true; } return false; @@ -185,6 +196,8 @@ private final Internals hgRepo; private HgException failure; private int lastWrittenFileSize; + private int lastFileMode; + private int lastFileModificationTime; CheckoutWorker(Internals implRepo) { hgRepo = implRepo; @@ -196,10 +209,11 @@ HgDataFile df = hgRepo.getRepo().getFileNode(fname); int fileRevIndex = df.getRevisionIndex(nid); // check out files based on manifest - // FIXME links! workingDirWriter = new WorkingDirFileWriter(hgRepo); - workingDirWriter.processFile(df, fileRevIndex); + workingDirWriter.processFile(df, fileRevIndex, flags); lastWrittenFileSize = workingDirWriter.bytesWritten(); + lastFileMode = workingDirWriter.fmode(); + lastFileModificationTime = workingDirWriter.mtime(); return true; } catch (IOException ex) { failure = new HgIOException("Failed to write down file revision", ex, workingDirWriter.getDestinationFile()); @@ -209,7 +223,15 @@ return false; } - public int getLastWrittenFileSize() { + public int getLastFileMode() { + return lastFileMode; + } + + public int getLastFileModificationTime() { + return lastFileModificationTime; + } + + public int getLastFileSize() { return lastWrittenFileSize; } diff -r 36e36b926747 -r bd5926e24aa3 src/org/tmatesoft/hg/internal/DirstateBuilder.java --- a/src/org/tmatesoft/hg/internal/DirstateBuilder.java Wed Apr 17 16:06:10 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/DirstateBuilder.java Fri Apr 19 20:30:34 2013 +0200 @@ -62,17 +62,7 @@ parent2 = p2 == null ? Nodeid.NULL : p2; } - public void recordNormal(Path fname, Flags flags, int bytesWritten) { - // Mercurial seems to write "n 0 -1 unset fname" on `hg --clean co -rev ` - // and the reason for 'force lookup' I suspect is a slight chance of simultaneous modification - // of the file by user that doesn't alter its size the very second dirstate is being written - // (or the file is being updated and the update brought in changes that didn't alter the file size - - // with size and timestamp set, later `hg status` won't notice these changes) - - // However, as long as we use this class to write clean copies of the files, we can put all the fields - // right away. - int fmode = flags == Flags.RegularFile ? 0666 : 0777; // FIXME actual unix flags - int mtime = (int) (System.currentTimeMillis() / 1000); + public void recordNormal(Path fname, int fmode, int mtime, int bytesWritten) { forget(fname); normal.put(fname, new HgDirstate.Record(fmode, bytesWritten, mtime, fname, null)); } diff -r 36e36b926747 -r bd5926e24aa3 src/org/tmatesoft/hg/internal/FileSystemHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/FileSystemHelper.java Fri Apr 19 20:30:34 2013 +0200 @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal; + +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.tmatesoft.hg.core.SessionContext; + +/** + * TODO Merge with RegularFileStats + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class FileSystemHelper { + + private final SessionContext ctx; + private final List linkCmd, chmodCmd, statCmd; + private final ProcessExecHelper execHelper; + + public FileSystemHelper(SessionContext sessionContext) { + ctx = sessionContext; + if (Internals.runningOnWindows()) { + linkCmd = Arrays.asList("mklink", "%1", "%2"); + chmodCmd = Collections.emptyList(); + statCmd = Collections.emptyList(); + } else { + linkCmd = Arrays.asList("/bin/ln", "-s", "%2", "%1"); + chmodCmd = Arrays.asList("/bin/chmod", "+x", "%1"); + statCmd = Arrays.asList("stat", "--format=%a", "%1"); + } + execHelper = new ProcessExecHelper(); + } + + public void createSymlink(File parentDir, String linkName, byte[] target) throws IOException { + ArrayList command = new ArrayList(linkCmd); + command.set(command.indexOf("%1"), linkName); + String targetFilename = Internals.getFileEncoding(ctx).decode(ByteBuffer.wrap(target)).toString(); + command.set(command.indexOf("%2"), targetFilename); + execHelper.cwd(parentDir); + try { + execHelper.exec(command); + } catch (InterruptedException ex) { + throw new IOException(ex); + } + } + + public void setExecutableBit(File parentDir, String fname) throws IOException { + if (chmodCmd.isEmpty()) { + return; + } + ArrayList command = new ArrayList(chmodCmd); + command.set(command.indexOf("%1"), fname); + execHelper.cwd(parentDir); + try { + execHelper.exec(command); + } catch (InterruptedException ex) { + throw new IOException(ex); + } + } + + public int getFileMode(File file, int defaultValue) throws IOException { + if (statCmd.isEmpty()) { + return defaultValue; + } + ArrayList command = new ArrayList(statCmd); + command.set(command.indexOf("%1"), file.getAbsolutePath()); + String result = null; + try { + result = execHelper.exec(command).toString().trim(); + if (result.isEmpty()) { + return defaultValue; + } + return Integer.parseInt(result, 8); + } catch (InterruptedException ex) { + throw new IOException(ex); + } catch (NumberFormatException ex) { + ctx.getLog().dump(getClass(), Warn, ex, String.format("Bad value for access rights:%s", result)); + return defaultValue; + } + } +} diff -r 36e36b926747 -r bd5926e24aa3 src/org/tmatesoft/hg/internal/RevlogStream.java --- a/src/org/tmatesoft/hg/internal/RevlogStream.java Wed Apr 17 16:06:10 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/RevlogStream.java Fri Apr 19 20:30:34 2013 +0200 @@ -67,7 +67,7 @@ } /*package*/ DataAccess getIndexStream() { - // FIXME post 1.0 must supply a hint that I'll need really few bytes of data (perhaps, at some offset) + // FIXME [1.1] must supply a hint that I'll need really few bytes of data (perhaps, at some offset) // to avoid mmap files when only few bytes are to be read (i.e. #dataLength()) return dataAccess.createReader(indexFile); } diff -r 36e36b926747 -r bd5926e24aa3 src/org/tmatesoft/hg/internal/WorkingDirFileWriter.java --- a/src/org/tmatesoft/hg/internal/WorkingDirFileWriter.java Wed Apr 17 16:06:10 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/WorkingDirFileWriter.java Fri Apr 19 20:30:34 2013 +0200 @@ -16,6 +16,8 @@ */ package org.tmatesoft.hg.internal; +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -23,6 +25,7 @@ import java.nio.channels.FileChannel; import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgManifest; import org.tmatesoft.hg.util.ByteChannel; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.LogFacility.Severity; @@ -37,29 +40,65 @@ private final Internals hgRepo; + private final boolean execCap, symlinkCap; + private final FileSystemHelper fileFlagsHelper; private File dest; private FileChannel destChannel; private int totalBytesWritten; + private ByteArrayChannel linkChannel; + private int fmode; public WorkingDirFileWriter(Internals internalRepo) { hgRepo = internalRepo; + execCap = Internals.checkSupportsExecutables(internalRepo.getRepo().getWorkingDir()); + symlinkCap = Internals.checkSupportsSymlinks(internalRepo.getRepo().getWorkingDir()); + if (symlinkCap || execCap) { + fileFlagsHelper = new FileSystemHelper(internalRepo.getSessionContext()); + } else { + fileFlagsHelper = null; + } } /** - * Information purposes only, to find out trouble location if {@link #processFile(HgDataFile, int)} fails + * Writes content of specified file revision into local filesystem, or create a symlink according to flags. + * Executable bit is set if specified and filesystem supports it. */ - public File getDestinationFile() { - return dest; - } - - public void processFile(HgDataFile df, int fileRevIndex) throws IOException { + public void processFile(HgDataFile df, int fileRevIndex, HgManifest.Flags flags) throws IOException { try { prepare(df.getPath()); + if (flags != HgManifest.Flags.Link) { + destChannel = new FileOutputStream(dest).getChannel(); + } else { + linkChannel = new ByteArrayChannel(); + } df.contentWithFilters(fileRevIndex, this); } catch (CancelledException ex) { hgRepo.getSessionContext().getLog().dump(getClass(), Severity.Error, ex, "Our impl doesn't throw cancellation"); + } finally { + if (flags != HgManifest.Flags.Link) { + destChannel.close(); + destChannel = null; + // leave dest in case anyone enquires with #getDestinationFile + } } - finish(); + if (linkChannel != null && symlinkCap) { + assert flags == HgManifest.Flags.Link; + fileFlagsHelper.createSymlink(dest.getParentFile(), dest.getName(), linkChannel.toArray()); + } else if (flags == HgManifest.Flags.Exec && execCap) { + fileFlagsHelper.setExecutableBit(dest.getParentFile(), dest.getName()); + } + // Although HgWCStatusCollector treats 644 (`hg manifest -v`) and 664 (my fs) the same, it's better + // to detect actual flags here + fmode = flags.fsMode(); // default to one from manifest + if (fileFlagsHelper != null) { + // if neither execBit nor link is supported by fs, it's unlikely file mode is supported, too. + try { + fmode = fileFlagsHelper.getFileMode(dest, fmode); + } catch (IOException ex) { + // Warn, we've got default value and can live with it + hgRepo.getSessionContext().getLog().dump(getClass(), Warn, ex, "Failed get file access rights"); + } + } } public void prepare(Path fname) throws IOException { @@ -68,22 +107,39 @@ if (fpath.indexOf('/') != -1) { dest.getParentFile().mkdirs(); } - destChannel = new FileOutputStream(dest).getChannel(); + destChannel = null; + linkChannel = null; totalBytesWritten = 0; + fmode = 0; } public int write(ByteBuffer buffer) throws IOException, CancelledException { - int written = destChannel.write(buffer); + final int written; + if (linkChannel != null) { + written = linkChannel.write(buffer); + } else { + written = destChannel.write(buffer); + } totalBytesWritten += written; return written; } - public void finish() throws IOException { - destChannel.close(); - dest = null; + /** + * Information purposes only, to find out trouble location if {@link #processFile(HgDataFile, int)} fails + */ + public File getDestinationFile() { + return dest; } public int bytesWritten() { return totalBytesWritten; } + + public int fmode() { + return fmode; + } + + public int mtime() { + return (int) (dest.lastModified() / 1000); + } } diff -r 36e36b926747 -r bd5926e24aa3 src/org/tmatesoft/hg/util/FileWalker.java --- a/src/org/tmatesoft/hg/util/FileWalker.java Wed Apr 17 16:06:10 2013 +0200 +++ b/src/org/tmatesoft/hg/util/FileWalker.java Fri Apr 19 20:30:34 2013 +0200 @@ -41,6 +41,8 @@ private RegularFileInfo nextFile; private Path nextPath; + // TODO FileWalker to accept SessionContext.Source and SessionContext to implement SessionContext.Source + // (if it doesn't break binary compatibility) public FileWalker(SessionContext ctx, File dir, Path.Source pathFactory) { this(ctx, dir, pathFactory, null); } diff -r 36e36b926747 -r bd5926e24aa3 test/org/tmatesoft/hg/test/TestCheckout.java --- a/test/org/tmatesoft/hg/test/TestCheckout.java Wed Apr 17 16:06:10 2013 +0200 +++ b/test/org/tmatesoft/hg/test/TestCheckout.java Fri Apr 19 20:30:34 2013 +0200 @@ -30,6 +30,8 @@ import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.FileInfo; +import org.tmatesoft.hg.util.FileWalker; import org.tmatesoft.hg.util.Pair; import org.tmatesoft.hg.util.Path; @@ -115,6 +117,36 @@ errorCollector.assertEquals("test", repo.getWorkingCopyBranchName()); } + + @Test + public void testCheckoutLinkAndExec() throws Exception { + File testRepoLoc = cloneRepoToTempLocation("test-flags", "test-checkout-flags", true); + repo = new HgLookup().detect(testRepoLoc); + new HgCheckoutCommand(repo).clean(true).changeset(0).execute(); + + FileWalker fw = new FileWalker(repo.getSessionContext(), testRepoLoc, new Path.SimpleSource()); + int execFound, linkFound, regularFound; + execFound = linkFound = regularFound = 0; + while(fw.hasNext()) { + fw.next(); + FileInfo fi = fw.file(); + boolean executable = fi.isExecutable(); + boolean symlink = fi.isSymlink(); + if (executable) { + execFound++; + } + if (symlink) { + linkFound++; + } + if (!executable && !symlink) { + regularFound++; + } + } + // TODO alter expected values to pass on Windows + errorCollector.assertEquals("Executable files", 1, execFound); + errorCollector.assertEquals("Symlink files", 1, linkFound); + errorCollector.assertEquals("Regular files", 1, regularFound); + } private static final class FilesOnlyFilter implements FileFilter { public boolean accept(File f) {