changeset 580:bd5926e24aa3

Respect unix flags for checkout/revert
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Fri, 19 Apr 2013 20:30:34 +0200
parents 36e36b926747
children 0890628ed51e
files src/org/tmatesoft/hg/core/HgCheckoutCommand.java src/org/tmatesoft/hg/internal/DirstateBuilder.java src/org/tmatesoft/hg/internal/FileSystemHelper.java src/org/tmatesoft/hg/internal/RevlogStream.java src/org/tmatesoft/hg/internal/WorkingDirFileWriter.java src/org/tmatesoft/hg/util/FileWalker.java test/org/tmatesoft/hg/test/TestCheckout.java
diffstat 7 files changed, 235 insertions(+), 29 deletions(-) [+]
line wrap: on
line diff
--- 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 <earlier 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;
 		}
 		
--- 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 <earlier 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));
 	}
--- /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<String> 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<String> command = new ArrayList<String>(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<String> command = new ArrayList<String>(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<String> command = new ArrayList<String>(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;
+		}
+	}
+}
--- 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);
 	}
--- 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);
+	}
 }
--- 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);
 	}
--- 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) {