This is an automated email from the git hooks/post-receive script. New commit to branch develop in repository scmwebeditor. See http://git.nuiton.org/scmwebeditor.git commit c8f2921cc746b8a1ec4d030d314f6a881729e5c8 Author: Hugo PIGEON <hpigeon@codelutin.com> Date: Thu Jun 4 17:29:50 2015 +0200 Add the ability to view the edited file's history --- .../org/nuiton/scmwebeditor/git/GitConnection.java | 160 +++++++++++++- .../org/nuiton/scmwebeditor/api/ScmConnection.java | 28 ++- .../org/nuiton/scmwebeditor/api/ScmRevision.java | 40 ++++ .../org/nuiton/scmwebeditor/svn/SvnConnection.java | 242 +++++++++++++++------ .../scmwebeditor/uiweb/actions/EditAction.java | 25 +++ .../uiweb/actions/ViewHistoryAction.java | 201 +++++++++++++++++ .../i18n/scmwebeditor-ui-web_en_GB.properties | 2 + .../i18n/scmwebeditor-ui-web_fr_FR.properties | 2 + swe-ui-web/src/main/resources/struts.xml | 4 + .../main/webapp/WEB-INF/content/imageViewer.jsp | 7 +- .../webapp/WEB-INF/content/modificationViewer.jsp | 46 +++- .../main/webapp/WEB-INF/content/viewHistory.jsp | 44 ++++ swe-ui-web/src/main/webapp/css/main.css | 62 ++++-- 13 files changed, 760 insertions(+), 103 deletions(-) diff --git a/swe-git/src/main/java/org/nuiton/scmwebeditor/git/GitConnection.java b/swe-git/src/main/java/org/nuiton/scmwebeditor/git/GitConnection.java index 347a4f8..03eee50 100644 --- a/swe-git/src/main/java/org/nuiton/scmwebeditor/git/GitConnection.java +++ b/swe-git/src/main/java/org/nuiton/scmwebeditor/git/GitConnection.java @@ -29,6 +29,7 @@ import org.eclipse.jgit.api.*; import org.eclipse.jgit.api.errors.*; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; @@ -37,8 +38,10 @@ import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; import org.nuiton.scmwebeditor.api.RepositoryNotFoundException; import org.nuiton.scmwebeditor.api.ScmConnection; +import org.nuiton.scmwebeditor.api.ScmRevision; import org.nuiton.scmwebeditor.api.dto.BrowseDto; import org.nuiton.scmwebeditor.api.dto.CommitDto; import org.nuiton.scmwebeditor.api.dto.result.BrowseResultDto; @@ -46,15 +49,10 @@ import org.nuiton.scmwebeditor.api.dto.result.CommitResultDto; import org.nuiton.scmwebeditor.api.dto.result.RemoveFileResultDto; import javax.naming.AuthenticationException; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; +import java.util.*; /** * Implementation of the Git's main features @@ -521,6 +519,154 @@ public class GitConnection implements ScmConnection { return path; } + @Override + public Map<ScmRevision, String> getRevisions(String address, String username, String password) { + + final int MAX_MESSAGE_LENGTH = 32; + String pathOnRepo = address.replace(addressGit + "/", ""); + Map<ScmRevision, String> revisions = new TreeMap<ScmRevision, String>(); + + try { + updateRepository(username, password); + } catch (RepositoryNotFoundException e) { + + if (log.isErrorEnabled()) { + log.error("Error while cloning or pulling the repository", e); + } + return null; + + } catch (IOException e) { + + if (log.isErrorEnabled()) { + log.error("Error while cloning or pulling the repository", e); + } + return null; + + } catch (AuthenticationException e) { + + if (log.isErrorEnabled()) { + log.error("Error while cloning or pulling the repository", e); + } + return null; + } + + try { + // getting the revisions + Git git = Git.open(localDirectory); + + LogCommand logCmd = git.log(); + logCmd.all(); + logCmd.addPath(pathOnRepo); + + try { + Iterable<RevCommit> commits = logCmd.call(); + + // making the value and the key to put in the Map + for (RevCommit commit : commits) { + + // getting the commit message + String msg = commit.getShortMessage(); + + if (msg.length() > MAX_MESSAGE_LENGTH) { + msg = msg.substring(0, MAX_MESSAGE_LENGTH) + "..."; + } + + // getting the date + long commitTime = ((long) commit.getCommitTime()) * 1000; + + Date commitDate = new Date(commitTime); + msg += " - " + commitDate; + + // adding the revision to the Map + ScmRevision scmRev = new ScmRevision(commit.getName(), commitTime); + revisions.put(scmRev, msg); + } + + } catch (GitAPIException e) { + if (log.isErrorEnabled()) { + log.error("Can not get revisions for address " + address, e); + } + return null; + } + } catch (IOException e) { + if (log.isErrorEnabled()) { + log.error("Can not open git local repository : " + localDirectory.getAbsolutePath(), e); + } + return null; + } + + return revisions; + } + + @Override + public File getFileContentAtRevision(String path, String username, String password, + String revision) throws AuthenticationException { + + try { + updateRepository(username, password); + } catch (RepositoryNotFoundException e) { + + if (log.isErrorEnabled()) { + log.error("Error while cloning or pulling the repository", e); + } + + } catch (IOException e) { + + if (log.isErrorEnabled()) { + log.error("Error while cloning or pulling the repository", e); + } + + } catch (AuthenticationException e) { + + if (log.isErrorEnabled()) { + log.error("Error while cloning or pulling the repository", e); + } + } + + String pathOnRepo = path.replace(addressGit + "/", ""); + + String tempFileName = localDirectory.getAbsolutePath() + File.separator + pathOnRepo + "_" + revision; + File tempFile = new File(tempFileName); + + ObjectId lastCommitId; + + try { + + lastCommitId = gitRepo.resolve(revision); + + // a RevWalk allows to walk over commits based on some filtering that is defined + RevWalk revWalk = new RevWalk(gitRepo); + RevCommit commit = revWalk.parseCommit(lastCommitId); + // and using commit's tree find the path + RevTree tree = commit.getTree(); + + // now try to find a specific file + TreeWalk treeWalk = new TreeWalk(gitRepo); + treeWalk.addTree(tree); + treeWalk.setRecursive(true); + treeWalk.setFilter(PathFilter.create(pathOnRepo)); + if (!treeWalk.next()) { + throw new IllegalStateException("Did not find expected file '" + pathOnRepo + "'"); + } + + ObjectId objectId = treeWalk.getObjectId(0); + ObjectLoader loader = gitRepo.open(objectId); + + // and then one can the loader to read the file + FileOutputStream fos = new FileOutputStream(tempFile); + loader.copyTo(fos); + + revWalk.dispose(); + + } catch (IOException e) { + if (log.isErrorEnabled()) { + log.error("Error while getting file '" + pathOnRepo + "' content at revision " + revision, e); + } + } + + return tempFile; + } + /** * Changing for another branch diff --git a/swe-scm-api/src/main/java/org/nuiton/scmwebeditor/api/ScmConnection.java b/swe-scm-api/src/main/java/org/nuiton/scmwebeditor/api/ScmConnection.java index b0f23f5..6e70f7a 100644 --- a/swe-scm-api/src/main/java/org/nuiton/scmwebeditor/api/ScmConnection.java +++ b/swe-scm-api/src/main/java/org/nuiton/scmwebeditor/api/ScmConnection.java @@ -26,6 +26,8 @@ import org.nuiton.scmwebeditor.api.dto.result.*; import javax.naming.AuthenticationException; import java.io.File; +import java.util.List; +import java.util.Map; /** * An interface which gives the SCMs main features @@ -49,7 +51,7 @@ public interface ScmConnection { /** - * Gives the content of a file as a String + * Gives the content of a file * @param path the path to the file to get the content from * @param username the user's login for the SCM * @param password the user's password for the SCM @@ -91,4 +93,28 @@ public interface ScmConnection { * @return the path to use to display an image on a web page */ String getImagePath(String address, String repositoryRoot); + + + /** + * Gives a list of the important revisions for the file at the given address + * @param address the file's address + * @param username the username to use to connect to the repository + * @param password the password to use to connect to the repository + * @return a list of revisions for the file at the given address with their revision name + * @throws AuthenticationException if there is a problem during the authentication process + */ + Map<ScmRevision, String> getRevisions(String address, String username, String password) throws AuthenticationException; + + + /** + * Gives the content of a file at the specified revision + * @param path the path to the file to get the content from + * @param username the user's login for the SCM + * @param password the user's password for the SCM + * @param revision the revision ID to use to get the file content + * @return a String which contains the file's content + * @throws AuthenticationException if there is a problem during the authentication process + */ + File getFileContentAtRevision(String path, String username, String password, String revision) + throws AuthenticationException; } diff --git a/swe-scm-api/src/main/java/org/nuiton/scmwebeditor/api/ScmRevision.java b/swe-scm-api/src/main/java/org/nuiton/scmwebeditor/api/ScmRevision.java new file mode 100644 index 0000000..fa78009 --- /dev/null +++ b/swe-scm-api/src/main/java/org/nuiton/scmwebeditor/api/ScmRevision.java @@ -0,0 +1,40 @@ +package org.nuiton.scmwebeditor.api; + +public class ScmRevision implements Comparable { + + /** the revision identifier (or revision number) */ + protected String revisionId; + + /** the commit time for this revision, in milliseconds */ + protected long commitTime; + + + public String getRevisionId() { return revisionId; } + + public void setRevisionId(String revisionId) { this.revisionId = revisionId; } + + public long getCommitTime() { return commitTime; } + + public void setCommitTime(long commitTime) { this.commitTime = commitTime; } + + + public ScmRevision(String revisionId, long commitTime) { + this.revisionId = revisionId; + this.commitTime = commitTime; + } + + @Override + public String toString() { + return revisionId; + } + + @Override + public int compareTo(Object o) { + + int returnVal = Long.compare(commitTime, ((ScmRevision) o).getCommitTime()); + + returnVal *= -1; // to get the revisions from the last one to the oldest one + + return returnVal; + } +} diff --git a/swe-svn/src/main/java/org/nuiton/scmwebeditor/svn/SvnConnection.java b/swe-svn/src/main/java/org/nuiton/scmwebeditor/svn/SvnConnection.java index c9a1e7a..baa9116 100644 --- a/swe-svn/src/main/java/org/nuiton/scmwebeditor/svn/SvnConnection.java +++ b/swe-svn/src/main/java/org/nuiton/scmwebeditor/svn/SvnConnection.java @@ -25,6 +25,7 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuiton.scmwebeditor.api.ScmConnection; +import org.nuiton.scmwebeditor.api.ScmRevision; import org.nuiton.scmwebeditor.api.dto.BrowseDto; import org.nuiton.scmwebeditor.api.dto.CommitDto; import org.nuiton.scmwebeditor.api.dto.result.BrowseResultDto; @@ -35,13 +36,20 @@ import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory; import org.tmatesoft.svn.core.internal.util.SVNEncodingUtil; import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions; +import org.tmatesoft.svn.core.io.SVNFileRevision; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; import org.tmatesoft.svn.core.wc.*; import javax.naming.AuthenticationException; import java.io.*; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Collection; +import java.util.Date; +import java.util.Map; +import java.util.TreeMap; /** * Implementation of the SVN's main features @@ -428,9 +436,167 @@ public class SvnConnection implements ScmConnection { @Override public File getFileContent(String path, String username, String password) throws AuthenticationException { + File fileContent = getFileContentAtRevision(path, username, password, "-1"); + + return fileContent; + } + + + @Override + public String getHeadRevisionNumber(String path, String username, String password) throws AuthenticationException { + + if (log.isDebugEnabled()) { + log.debug("headRevisionNumber expected " + addressSvn + " ; got " + path); + } + + ISVNAuthenticationManager svnAuthManager = SVNWCUtil.createDefaultAuthenticationManager(username, password); + + DefaultSVNOptions svnOptions = new DefaultSVNOptions(); + svnOptions.setPropertyValue(SVNProperty.EOL_STYLE, SVNProperty.EOL_STYLE_LF); + + SVNWCClient wcClient = new SVNWCClient(svnAuthManager, svnOptions); + + SVNInfo info = null; + + try { + info = wcClient.doInfo(SVNURL.parseURIEncoded(path), SVNRevision.HEAD, SVNRevision.HEAD); + } catch (SVNAuthenticationException e) { + throw new AuthenticationException("Auth fail"); + } catch (SVNException e) { + if (log.isErrorEnabled()) { + log.error("Can not get info from SVN repository", e); + } + } + + String headRevision = ""; + + if (info != null) { + headRevision = info.getRevision().toString(); + } + + return headRevision; + } + + + @Override + public String getRepositoryId() { + String repositoryUUID; + try { + String encodedUrl = SVNEncodingUtil.autoURIEncode(addressSvn); + SVNRepository repository = SVNRepositoryFactory.create(SVNURL.parseURIEncoded(encodedUrl)); + ISVNAuthenticationManager svnAuthManager = SVNWCUtil.createDefaultAuthenticationManager(); + repository.setAuthenticationManager(svnAuthManager); + + repositoryUUID = repository.getRepositoryUUID(true); + } catch (SVNException e) { + if (log.isDebugEnabled()) { + log.debug("Can't get UUID", e); + } + return null; + } + + return repositoryUUID; + } + + + @Override + public String getFileName() { + return fileName; + } + + @Override + public String getImagePath(String address, String repositoryRoot) { + return address; + } + + @Override + public Map<ScmRevision, String> getRevisions(String address, String username, String password) throws AuthenticationException { + + final int MAX_MESSAGE_LENGTH = 32; + + String url = address.substring(0, address.lastIndexOf('/')); + String file = address.substring(address.lastIndexOf('/') + 1); + + Map<ScmRevision, String> revisions = new TreeMap<ScmRevision, String>(); + + updateAuthentication(username, password); + + SVNRepository repository; + + try { + // getting the revisions + repository = SVNRepositoryFactory.create(SVNURL.parseURIEncoded(url)); + + ISVNAuthenticationManager svnAuthManager = SVNWCUtil.createDefaultAuthenticationManager(username, password); + repository.setAuthenticationManager(svnAuthManager); + + SVNNodeKind nodeKind = repository.checkPath(file, -1); + + if (nodeKind == SVNNodeKind.NONE) { + if (log.isErrorEnabled()) { + log.error("There is no entry at '" + address + "'."); + } + throw new IllegalArgumentException("There is no entry at '" + address + "'."); + } else if (nodeKind == SVNNodeKind.DIR) { + if (log.isErrorEnabled()) { + log.error("The entry at '" + address + "' is a file while a directory was expected."); + } + throw new IllegalArgumentException("The entry at '" + address + "' is a directory while a file was expected."); + } + + Collection fileRevisions; + fileRevisions = repository.getFileRevisions(file, null, 0, repository.getLatestRevision()); + + // making the value and the key to put in the Map + for (Object fileRev : fileRevisions) { + + // getting the commit message + String msg = ((SVNFileRevision) fileRev).getRevisionProperties().getStringValue(SVNRevisionProperty.LOG); + if (msg.length() > MAX_MESSAGE_LENGTH) { + msg = msg.substring(0, MAX_MESSAGE_LENGTH) + "..."; + } + + long revNum = ((SVNFileRevision) fileRev).getRevision(); + msg += " (rev " + revNum + ")"; + + // getting the date + String date = ((SVNFileRevision) fileRev).getRevisionProperties().getStringValue(SVNRevisionProperty.DATE); + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"); + Date commitDate = null; + try { + commitDate = df.parse(date); + } catch (ParseException e) { + if (log.isErrorEnabled()) { + log.error("Can not parse date " + date); + } + } + msg += " - " + commitDate; + + // adding the revision to the Map + ScmRevision scmRev = new ScmRevision(String.valueOf(revNum), commitDate.getTime()); + revisions.put(scmRev, msg); + } + + } catch (SVNAuthenticationException e) { + throw new AuthenticationException("Auth fail"); + } catch (SVNException e) { + if (log.isErrorEnabled()) { + log.error("Can not get revisions from SVN repository for address " + address, e); + } + } + + return revisions; + } + + @Override + public File getFileContentAtRevision(String path, String username, String password, + String revision) throws AuthenticationException { + String url = path.substring(0, path.lastIndexOf('/')); String file = path.substring(path.lastIndexOf('/') + 1); + long rev = Long.parseLong(revision); + // storing the file content to the user's local directory File localDirectory = new File(pathToLocalRepos); @@ -461,19 +627,19 @@ public class SvnConnection implements ScmConnection { ISVNAuthenticationManager svnAuthManager = SVNWCUtil.createDefaultAuthenticationManager(username, password); repository.setAuthenticationManager(svnAuthManager); - SVNNodeKind nodeKind = repository.checkPath(file, -1); + SVNNodeKind nodeKind = repository.checkPath(file, rev); if (nodeKind == SVNNodeKind.NONE) { if (log.isErrorEnabled()) { log.error("There is no entry at '" + url + "'."); } - throw new IllegalArgumentException("There is no entry at '" + url + "'."); + return null; } else if (nodeKind == SVNNodeKind.DIR) { if (log.isErrorEnabled()) { log.error("The entry at '" + url + "' is a file while a directory was expected."); } - throw new IllegalArgumentException("The entry at '" + url + "' is a file while a directory was expected."); + return null; } @@ -481,7 +647,7 @@ public class SvnConnection implements ScmConnection { ByteArrayOutputStream baos = new ByteArrayOutputStream(); SVNProperties fileProperties = new SVNProperties(); - repository.getFile(file, -1, fileProperties, baos); + repository.getFile(file, rev, fileProperties, baos); fileProperties.getStringValue(SVNProperty.REVISION); try { @@ -517,74 +683,6 @@ public class SvnConnection implements ScmConnection { } - @Override - public String getHeadRevisionNumber(String path, String username, String password) throws AuthenticationException { - - if (log.isDebugEnabled()) { - log.debug("headRevisionNumber expected " + addressSvn + " ; got " + path); - } - - ISVNAuthenticationManager svnAuthManager = SVNWCUtil.createDefaultAuthenticationManager(username, password); - - DefaultSVNOptions svnOptions = new DefaultSVNOptions(); - svnOptions.setPropertyValue(SVNProperty.EOL_STYLE, SVNProperty.EOL_STYLE_LF); - - SVNWCClient wcClient = new SVNWCClient(svnAuthManager, svnOptions); - - SVNInfo info = null; - - try { - info = wcClient.doInfo(SVNURL.parseURIEncoded(path), SVNRevision.HEAD, SVNRevision.HEAD); - } catch (SVNAuthenticationException e) { - throw new AuthenticationException("Auth fail"); - } catch (SVNException e) { - if (log.isErrorEnabled()) { - log.error("Can not get info from SVN repository", e); - } - } - - String headRevision = ""; - - if (info != null) { - headRevision = info.getRevision().toString(); - } - - return headRevision; - } - - - @Override - public String getRepositoryId() { - String repositoryUUID; - try { - String encodedUrl = SVNEncodingUtil.autoURIEncode(addressSvn); - SVNRepository repository = SVNRepositoryFactory.create(SVNURL.parseURIEncoded(encodedUrl)); - ISVNAuthenticationManager svnAuthManager = SVNWCUtil.createDefaultAuthenticationManager(); - repository.setAuthenticationManager(svnAuthManager); - - repositoryUUID = repository.getRepositoryUUID(true); - } catch (SVNException e) { - if (log.isDebugEnabled()) { - log.debug("Can't get UUID", e); - } - return null; - } - - return repositoryUUID; - } - - - @Override - public String getFileName() { - return fileName; - } - - @Override - public String getImagePath(String address, String repositoryRoot) { - return address; - } - - public String getSvnRoot(String username, String password) { String repositoryRoot; try { diff --git a/swe-ui-web/src/main/java/org/nuiton/scmwebeditor/uiweb/actions/EditAction.java b/swe-ui-web/src/main/java/org/nuiton/scmwebeditor/uiweb/actions/EditAction.java index 9f50b38..94d07f6 100644 --- a/swe-ui-web/src/main/java/org/nuiton/scmwebeditor/uiweb/actions/EditAction.java +++ b/swe-ui-web/src/main/java/org/nuiton/scmwebeditor/uiweb/actions/EditAction.java @@ -29,6 +29,7 @@ import org.apache.shiro.crypto.BlowfishCipherService; import org.nuiton.scmwebeditor.api.OperationNotSupportedException; import org.nuiton.scmwebeditor.api.ScmConnection; import org.nuiton.scmwebeditor.api.ScmProvider; +import org.nuiton.scmwebeditor.api.ScmRevision; import org.nuiton.scmwebeditor.api.dto.result.AbstractResultDto; import org.nuiton.scmwebeditor.uiweb.ScmWebEditorConfig; @@ -40,6 +41,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.text.Normalizer; import java.util.LinkedList; +import java.util.Map; /** * Allows to edit a file @@ -66,6 +68,12 @@ public class EditAction extends ScmWebEditorMainAction { /** the interval between two automatic saves */ protected int autoSaveInterval; + /** a list of the revision which can be used to view the file's history */ + protected Map<ScmRevision, String> revisions; + + /** the selected revision to view */ + protected String revision; + public String getSelectedBranch() { return selectedBranch; } @@ -91,6 +99,15 @@ public class EditAction extends ScmWebEditorMainAction { public void setAutoSaveInterval(int autoSaveInterval) { this.autoSaveInterval = autoSaveInterval; } + public Map<ScmRevision, String> getRevisions() { return revisions; } + + public void setRevisions( + Map<ScmRevision, String> revisions) { this.revisions = revisions; } + + public String getRevision() { return revision; } + + public void setRevision(String revision) { this.revision = revision; } + /** * Execution of the edit action * @return a code interpreted in the file struts.xml @@ -225,6 +242,14 @@ public class EditAction extends ScmWebEditorMainAction { } } + try { + revisions = scmConn.getRevisions(address, username, pw); + } catch (AuthenticationException e) { + if (log.isErrorEnabled()) { + log.error("Can not get revisions for address " + address + " : authentication error", e); + } + } + /* * Getting the file and its revision diff --git a/swe-ui-web/src/main/java/org/nuiton/scmwebeditor/uiweb/actions/ViewHistoryAction.java b/swe-ui-web/src/main/java/org/nuiton/scmwebeditor/uiweb/actions/ViewHistoryAction.java new file mode 100644 index 0000000..7eb17a6 --- /dev/null +++ b/swe-ui-web/src/main/java/org/nuiton/scmwebeditor/uiweb/actions/ViewHistoryAction.java @@ -0,0 +1,201 @@ +/* + * #%L + * ScmWebEditor + * %% + * Copyright (C) 2009 - 2015 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/lgpl-3.0.html>. + * #L% + */ +package org.nuiton.scmwebeditor.uiweb.actions; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.shiro.codec.Base64; +import org.apache.shiro.crypto.BlowfishCipherService; +import org.nuiton.scmwebeditor.api.ScmConnection; +import org.nuiton.scmwebeditor.api.ScmProvider; +import org.nuiton.scmwebeditor.uiweb.ScmWebEditorConfig; + +import javax.naming.AuthenticationException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpSession; +import java.io.File; +import java.io.IOException; +import java.text.Normalizer; + +/** + * Allows to view the history of a file + */ +public class ViewHistoryAction extends ScmWebEditorMainAction { + + private static final Log log = LogFactory.getLog(ViewHistoryAction.class); + + /** the content of the file */ + protected String fileContent; + + /** the revision to use to get the file content */ + protected String revision; + + /** the error name or null if no error occurred */ + protected String error; + + + public String getFileContent() { return fileContent; } + + public void setFileContent(String fileContent) { this.fileContent = fileContent; } + + public String getRevision() { return revision; } + + public void setRevision(String revision) { this.revision = revision; } + + public String getError() { return error; } + + public void setError(String error) { this.error = error; } + + /** + * Execution of the view history action + * @return a code interpreted in the file struts.xml + */ + public String execute() { + + HttpSession session = request.getSession(); + String sessionId = session.getId(); + + error = null; + + String pathToLocalRepos = ScmWebEditorConfig.getLocalRepositoriesPath() + File.separator + sessionId; + + ScmProvider provider = ScmWebEditorConfig.getProvider(scmType); + ScmConnection scmConn = provider.getConnection(address, pathToLocalRepos); + + // if the repository is not protected, we get its UUID + String repositoryUUID = scmConn.getRepositoryId(); + if (repositoryUUID == null) { + repositoryUUID = address.replace(' ', '_'); + repositoryUUID = Normalizer.normalize(repositoryUUID, Normalizer.Form.NFD).replaceAll("[\u0300-\u036F]", ""); + } + + if (log.isDebugEnabled()) { + log.debug("Login : " + username); + } + + + /* + * Reading the cookie + */ + + + String usernamepwCookie = null; + // read the cookies + + BlowfishCipherService bf = new BlowfishCipherService(); + + byte[] privateKey = Base64.decode(ScmWebEditorConfig.getKey()); + + if (request.getCookies() != null) { + for (Cookie c : request.getCookies()) { + if (c.getName().equals(repositoryUUID)) { + usernamepwCookie = c.getValue(); + } + } + } + + if (usernamepwCookie != null) { + + String usernameDecode = new String(bf.decrypt(Base64.decode(usernamepwCookie), privateKey).getBytes()); + + String[] resCookie = usernameDecode.split(","); + if (resCookie.length == 2) { + username = resCookie[0]; + pw = resCookie[1]; + } + } + + if (saveCookie) { + if (username != null && pw != null) { + + if (!username.equals("") && !pw.equals("")) { + Cookie authCookie = new Cookie(repositoryUUID, bf.encrypt((username + "," + pw).getBytes(), privateKey).toBase64()); + authCookie.setMaxAge(60 * 60 * 24 * 365); + response.addCookie(authCookie); + } + } + + } + + // authentication + String[] usernamePw = getUsernamePwFromSession(repositoryUUID, username, pw); + username = usernamePw[0]; + pw = usernamePw[1]; + + String name = username; + String password = pw; + + if (name == null) { + name = "anonymous"; + } + if (password == null) { + password = "anonymous"; + } + + + /* + * Getting the file's revision + */ + + try { + File tempFile = scmConn.getFileContentAtRevision(address, name, password, revision); + + if (tempFile != null) { + fileContent = FileUtils.readFileToString(tempFile); + } else { + error = ERROR; + return ERROR; + } + } catch (AuthenticationException e) { + request.setAttribute(PARAMETER_ADDRESS, address); + + // if scm authentication failed user is redirected on login page + if (log.isDebugEnabled()) { + log.debug("Auth Fail ", e); + } + + // deleting the cookies for this repository + for (Cookie c : request.getCookies()) { + if (c.getName().equals(repositoryUUID)) { + c.setMaxAge(0);//On supprime le cookie + response.addCookie(c); + if (log.isDebugEnabled()) { + log.debug("Cookie supprimé"); + } + } + } + + getScmSession().delScmUser(repositoryUUID); + error = LOGIN; + return LOGIN; + } catch (IOException e) { + if (log.isErrorEnabled()) { + log.error("Can not read temp file for " + address + " at revision " + revision, e); + } + error = ERROR; + return ERROR; + } + + return SUCCESS; + } +} diff --git a/swe-ui-web/src/main/resources/i18n/scmwebeditor-ui-web_en_GB.properties b/swe-ui-web/src/main/resources/i18n/scmwebeditor-ui-web_en_GB.properties index 6af700a..ac8b2db 100644 --- a/swe-ui-web/src/main/resources/i18n/scmwebeditor-ui-web_en_GB.properties +++ b/swe-ui-web/src/main/resources/i18n/scmwebeditor-ui-web_en_GB.properties @@ -27,6 +27,7 @@ scm.exitJavascript=Exit ScmWebEditor? scm.exitJavascriptChanges=Exit ScmWebEditor without saving? All modification will be lost. scm.exitTitle=Exit ScmWebEditor without saving. scm.fileModify=File modify while editing. +scm.fileRevision=File revision\: scm.fileToMove=File to move\: scm.forceSave=Force save scm.formTransferred=You should be transferred automatically to the form page. If not please @@ -108,5 +109,6 @@ scm.uploadTitle=Upload a file on the repository scm.uselessSave=It's useless to save the file, file is not modify. scm.username=Username scm.usernameTitle=Repository username +scm.viewHistory=View file history scm.welcome=Welcome on SCMWebEditor scm.yes=Yes diff --git a/swe-ui-web/src/main/resources/i18n/scmwebeditor-ui-web_fr_FR.properties b/swe-ui-web/src/main/resources/i18n/scmwebeditor-ui-web_fr_FR.properties index ed26f30..839afba 100644 --- a/swe-ui-web/src/main/resources/i18n/scmwebeditor-ui-web_fr_FR.properties +++ b/swe-ui-web/src/main/resources/i18n/scmwebeditor-ui-web_fr_FR.properties @@ -27,6 +27,7 @@ scm.exitJavascript=Quitter ScmWebEditor ? scm.exitJavascriptChanges=Quitter ScmWebEditor sans sauvegarder ? Toutes les modifications seront perdues. scm.exitTitle=Quitter ScmWebEditor sans sauvegarder. scm.fileModify=Fichier modifié pendant l'édition. +scm.fileRevision=Révision du fichier \: scm.fileToMove=Fichier à déplacer \: scm.forceSave=Forcer la sauvegarde scm.formTransferred=Vous devriez être redirigé vers la page du formulaire. si non @@ -108,5 +109,6 @@ scm.uploadTitle=Ajouter un fichier sur le dépôt scm.uselessSave=Inutile de sauvegarder le fichier, aucune modification n'a été apportée scm.username=Identifiant scm.usernameTitle=Identifiant du dépôt +scm.viewHistory=Voir l'historique du fichier scm.welcome=Bienvenue sur SCMWebEditor scm.yes=Oui diff --git a/swe-ui-web/src/main/resources/struts.xml b/swe-ui-web/src/main/resources/struts.xml index ec640c5..783b0e7 100644 --- a/swe-ui-web/src/main/resources/struts.xml +++ b/swe-ui-web/src/main/resources/struts.xml @@ -155,6 +155,10 @@ <result name="login">/WEB-INF/content/popups/createBranchForm.jsp</result> <result name="error">/WEB-INF/content/popups/createBranchForm.jsp</result> </action> + + <action name="viewHistory" class="org.nuiton.scmwebeditor.uiweb.actions.ViewHistoryAction"> + <result name="*">/WEB-INF/content/viewHistory.jsp</result> + </action> <action name="preview" class="org.nuiton.scmwebeditor.uiweb.actions.PreviewAction"> <result>/WEB-INF/content/preview.jsp</result> diff --git a/swe-ui-web/src/main/webapp/WEB-INF/content/imageViewer.jsp b/swe-ui-web/src/main/webapp/WEB-INF/content/imageViewer.jsp index 445f6da..4b731ca 100644 --- a/swe-ui-web/src/main/webapp/WEB-INF/content/imageViewer.jsp +++ b/swe-ui-web/src/main/webapp/WEB-INF/content/imageViewer.jsp @@ -129,7 +129,7 @@ <s:text name="scm.openAnotherFile"/> </s:set> -<s:submit id="openAnotherFile" value="%{openAnotherFile}" onclick="openPopin('openFilePopin'); return false;"/> +<s:submit id="openAnotherFileImg" value="%{openAnotherFile}" onclick="openPopin('openFilePopin'); return false;"/> <div id="buttonList"> @@ -203,7 +203,7 @@ <s:property value="address"/> </s:set> - <ul id="repositoryButtons"> + <ul id="repositoryButtonsImg"> <li> <s:submit name="uploadButton" value="%{scm.upload}" title="%{scm.uploadTitle}" onClick="javascript:open_popup('doUploadFile.action', 'upload', getElementById('address').value, '%{scmType}'); return false;"/> @@ -275,9 +275,6 @@ </div> </div> - <div id="targetContentUpload"></div> - - </div> <p align="right">©2004-2015 CodeLutin</p> </div> diff --git a/swe-ui-web/src/main/webapp/WEB-INF/content/modificationViewer.jsp b/swe-ui-web/src/main/webapp/WEB-INF/content/modificationViewer.jsp index 757b788..6c962c5 100644 --- a/swe-ui-web/src/main/webapp/WEB-INF/content/modificationViewer.jsp +++ b/swe-ui-web/src/main/webapp/WEB-INF/content/modificationViewer.jsp @@ -206,6 +206,12 @@ <s:submit id="openAnotherFile" value="%{openAnotherFile}" onclick="openPopin('openFilePopin'); return false;"/> +<s:set id="viewHistory"> + <s:text name="scm.viewHistory"/> +</s:set> + +<s:submit id="viewHistory" value="%{viewHistory}" onclick="openPopin('fileHistoryPopin'); return false;"/> + <div id="buttonList"> <!--BEGIN Save and continue --> @@ -483,9 +489,6 @@ </div> </form> - <div id="targetContentUpload"></div> - - </div> <p align="right">©2004-2015 CodeLutin</p> </div> @@ -589,6 +592,43 @@ </div> +<!-- popin to view the file history --> +<div class="popin" id="fileHistoryPopin"> + <span class="closePopin" onclick="closePopin('fileHistoryPopin')"> + X + </span> + + <h1><s:text name="scm.viewHistory"/></h1> + + <s:url id="ajaxViewHistory" value="viewHistory.action"/> + + <form id="viewHistoryForm"> + + <label for="revisionsList"><s:text name="scm.fileRevision"/></label><br/> + <s:select id="revisionsList" name="revision" list="revisions"/><br/> + + <sj:a + id="ajaxHistoryButton" + formIds="editForm,viewHistoryForm" + targets="htmlContentHistory" + href="%{ajaxViewHistory}" + title="%{scm.viewHistory}" + > + + <s:submit id="viewHistoryButton" value="%{scm.submit}" + onclick="document.getElementById('historyIndicator').style.display = 'inline'; return false;"/> + + <img src="struts/js/jstree/themes/classic/throbber.gif" alt="loading" class="indicator" id="historyIndicator"/> + + </sj:a> + + </form> + + <div id="htmlContentHistory"></div> + +</div> + + <div id="popinBackground"></div> <s:if test="autoSaveEnabled"> diff --git a/swe-ui-web/src/main/webapp/WEB-INF/content/viewHistory.jsp b/swe-ui-web/src/main/webapp/WEB-INF/content/viewHistory.jsp new file mode 100644 index 0000000..fab1295 --- /dev/null +++ b/swe-ui-web/src/main/webapp/WEB-INF/content/viewHistory.jsp @@ -0,0 +1,44 @@ +<%-- + #%L + ScmWebEditor + %% + Copyright (C) 2009 - 2015 CodeLutin + %% + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + 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 Lesser Public License for more details. + + You should have received a copy of the GNU General Lesser Public + License along with this program. If not, see + <http://www.gnu.org/licenses/lgpl-3.0.html>. + #L% + --%> + +<%@ page language="java" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8" %> + +<%@ taglib prefix="s" uri="/struts-tags" %> + +<script type="text/javascript"> + document.getElementById("historyIndicator").style.display = 'none'; +</script> + +<s:if test="error == null"> + +<pre id="historyContent"> +<s:property value="fileContent"/> +</pre> + +</s:if> +<s:elseif test="error.equals('login')"> + <s:text name="scm.badUsernameOrPassword"/> +</s:elseif> +<s:else> + <s:text name="scm.repoError"/> +</s:else> diff --git a/swe-ui-web/src/main/webapp/css/main.css b/swe-ui-web/src/main/webapp/css/main.css index 613fe13..4c3053f 100644 --- a/swe-ui-web/src/main/webapp/css/main.css +++ b/swe-ui-web/src/main/webapp/css/main.css @@ -225,8 +225,8 @@ ul.flags li { width:200px; height:32px; position:relative; - bottom:20px; - left: 70%; + bottom:30px; + left: 72%; } @@ -384,7 +384,7 @@ ul.flags li { } #preview { - overflow: scroll; + overflow: auto; height: 568px; } @@ -421,14 +421,15 @@ ul.flags li { height: 75%; top: 12%; left: 25%; - overflow: scroll; + overflow: auto; } -#autoSavePopin { - height: 75%; - top: 12%; - left: 25%; - overflow: scroll; +#fileHistoryPopin { + height: 90%; + top: 4%; + left: 4%; + overflow: auto; + width: 90%; } #popinBackground { @@ -451,7 +452,7 @@ ul.flags li { color: red; } -#ajaxSaveButton, #ajaxAutoSaveButton { +#ajaxSaveButton, #ajaxHistoryButton { text-decoration: none; } @@ -459,25 +460,41 @@ ul.flags li { display: none; } -#openAnotherFile { +#openAnotherFile, #openAnotherFileImg { position: relative; top: 20px; + right: 10%; +} + +#openAnotherFileImg { + right: 0; } -#wwctrl_openAnotherFile { +#wwctrl_openAnotherFile, #wwctrl_openAnotherFileImg, #wwctrl_viewHistory { text-align: center; } -ul#repositoryButtons { +#viewHistory { + position: relative; + bottom: 5px; + left: 10%; +} + +ul#repositoryButtons, ul#repositoryButtonsImg { display: table; list-style: none; border-spacing: 10px; position: relative; - bottom: 10px; + bottom: 30px; margin: auto; } -ul#repositoryButtons li { +ul#repositoryButtonsImg { + bottom: 10px; + left: 5%; +} + +ul#repositoryButtons li, ul#repositoryButtonsImg li { display: table-cell; } @@ -488,4 +505,19 @@ ul#repositoryButtons li { #displayedImage { display: block; margin: auto; +} + +#historyContent { + border: 1px solid black; + overflow: auto; + min-height: 150px; + padding: 5px; +} + +#wwctrl_viewHistoryButton { + display: inline; +} + +#viewHistoryButton { + margin-top: 10px; } \ No newline at end of file -- To stop receiving notification emails like this one, please contact nuiton.org SCM administrator <admin+scm@nuiton.org>.