diff --git a/src/main/java/org/craftercms/studio/api/v2/dal/ItemDAO.java b/src/main/java/org/craftercms/studio/api/v2/dal/ItemDAO.java index a8ff1c8256..1b183b397e 100644 --- a/src/main/java/org/craftercms/studio/api/v2/dal/ItemDAO.java +++ b/src/main/java/org/craftercms/studio/api/v2/dal/ItemDAO.java @@ -23,6 +23,7 @@ import org.craftercms.studio.api.v2.dal.item.LightItem; import org.springframework.transaction.annotation.Transactional; +import java.time.Instant; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -261,7 +262,8 @@ default void moveItem(String siteId, String previousPath, String newPath, Long parentId, String newPreviewUrl, String label, long userId) { moveItemInternal(siteId, previousPath, newPath, parentId, - newPreviewUrl, label, SAVE_AND_CLOSE_ON_MASK, SAVE_AND_CLOSE_OFF_MASK, userId); + newPreviewUrl, label, SAVE_AND_CLOSE_ON_MASK, SAVE_AND_CLOSE_OFF_MASK, + Instant.now(), userId, null, null, null, null); updatePreviousPath(siteId, previousPath, newPath); } @@ -290,12 +292,20 @@ default void moveItem(String siteId, String previousPath, String newPath, * @param offStatesBitMap state bitmap to flip off * @param userId user id of the user performing the move operation */ - void moveItemInternal(@Param(SITE_ID) String siteId, @Param(PREVIOUS_PATH) String previousPath, @Param(NEW_PATH) String newPath, + void moveItemInternal(@Param(SITE_ID) String siteId, + @Param(PREVIOUS_PATH) String previousPath, + @Param(NEW_PATH) String newPath, @Param(PARENT_ID) Long parentId, - @Param(NEW_PREVIEW_URL) String newPreviewUrl, @Param(LABEL) String label, + @Param(NEW_PREVIEW_URL) String newPreviewUrl, + @Param(LABEL) String label, @Param(ON_STATES_BIT_MAP) long onStatesBitMap, @Param(OFF_STATES_BIT_MAP) long offStatesBitMap, - @Param(USER_ID) long userId); + @Param(LAST_MODIFIED_ON) Instant lastModifiedOn, + @Param(USER_ID) long userId, + @Param(CONTENT_TYPE_ID) String contentTypeId, + @Param(SYSTEM_TYPE) String contentTypeClass, + @Param(MIME_TYPE) String mimeType, + @Param(SIZE) Long contentSize); /** * Get content items for given paths @@ -586,15 +596,33 @@ default void updateParentId(long siteId, Collection paths) { /** * Move item query for sync task * - * @param siteId site identifier - * @param previousPath previous path - * @param newPath new path - * @param onStatesBitMap state bitmap to flip on - * @param offStatesBitMap state bitmap to flip off - */ - void moveItemForSyncTask(@Param(SITE_ID) String siteId, @Param(PREVIOUS_PATH) String previousPath, @Param(NEW_PATH) String newPath, - @Param(ON_STATES_BIT_MAP) long onStatesBitMap, - @Param(OFF_STATES_BIT_MAP) long offStatesBitMap); + * @param siteId site identifier + * @param previousPath previous path + * @param newPath new path + * @param onStatesBitMap state bitmap to flip on + * @param offStatesBitMap state bitmap to flip off + * @param contentTypeId + * @param contentTypeClass + * @param mimeType + * @param contentSize + */ +// void moveItemForSyncTask(@Param(SITE_ID) long siteId, @Param(PREVIOUS_PATH) String previousPath, @Param(NEW_PATH) String newPath, +// @Param(ON_STATES_BIT_MAP) long onStatesBitMap, +// @Param(OFF_STATES_BIT_MAP) long offStatesBitMap); + + default void moveItemForSyncTask(String siteId, String previousPath, String newPath, + Long parentId, String newPreviewUrl, + String label, long userId, Instant lastModifiedOn, + long onStatesBitMap, long offStatesBitMap, + String contentTypeId, String contentTypeClass, + String mimeType, long contentSize) { + moveItemInternal(siteId, previousPath, newPath, parentId, + newPreviewUrl, label, onStatesBitMap, offStatesBitMap, + lastModifiedOn, userId, + contentTypeId, contentTypeClass, mimeType, contentSize); + updatePreviousPath(siteId, previousPath, newPath); + } + /** * Update item query for sync task diff --git a/src/main/java/org/craftercms/studio/impl/v1/util/ContentUtils.java b/src/main/java/org/craftercms/studio/impl/v1/util/ContentUtils.java index 5b6a87d45b..e240bc5b4c 100644 --- a/src/main/java/org/craftercms/studio/impl/v1/util/ContentUtils.java +++ b/src/main/java/org/craftercms/studio/impl/v1/util/ContentUtils.java @@ -31,7 +31,7 @@ import static java.lang.String.format; import static org.apache.commons.io.FilenameUtils.getFullPathNoEndSeparator; -import static org.apache.commons.lang3.StringUtils.removeEnd; +import static org.apache.commons.lang3.Strings.CS; import static org.craftercms.studio.api.v1.constant.DmConstants.SLASH_INDEX_FILE; import static org.craftercms.studio.api.v1.constant.StudioConstants.FILE_SEPARATOR; @@ -112,7 +112,7 @@ public static boolean matchesPatterns(String uri, List patterns) { * @return path of the parent item */ public static String getParentUrl(String path) { - return getFullPathNoEndSeparator(removeEnd(path, SLASH_INDEX_FILE)); + return getFullPathNoEndSeparator(CS.removeEnd(path, SLASH_INDEX_FILE)); } /** diff --git a/src/main/java/org/craftercms/studio/impl/v2/repository/GitContentRepositoryImpl.java b/src/main/java/org/craftercms/studio/impl/v2/repository/GitContentRepositoryImpl.java index c3db7a1dc4..38aa67dc18 100644 --- a/src/main/java/org/craftercms/studio/impl/v2/repository/GitContentRepositoryImpl.java +++ b/src/main/java/org/craftercms/studio/impl/v2/repository/GitContentRepositoryImpl.java @@ -59,6 +59,7 @@ import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.diff.DiffConfig; import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.RenameDetector; import org.eclipse.jgit.errors.StopWalkException; import org.eclipse.jgit.internal.storage.file.LockFile; import org.eclipse.jgit.lib.*; @@ -290,7 +291,11 @@ private List processDiffEntry(Git git, List diffEntrie long startMark = logger.isDebugEnabled() ? System.currentTimeMillis() : 0; List toReturn = new ArrayList<>(); - for (DiffEntry diffEntry : diffEntries) { + RenameDetector renameDetector = new RenameDetector(git.getRepository()); + renameDetector.addAll(diffEntries); + renameDetector.setRenameScore(10); +// Note that this rename score needs to be configured in history and any git diff'ing command + for (DiffEntry diffEntry : renameDetector.compute()) { // Update the paths to have a preceding separator String pathNew = FILE_SEPARATOR + diffEntry.getNewPath(); String pathOld = FILE_SEPARATOR + diffEntry.getOldPath(); diff --git a/src/main/java/org/craftercms/studio/impl/v2/sync/SyncFromRepositoryTask.java b/src/main/java/org/craftercms/studio/impl/v2/sync/SyncFromRepositoryTask.java index 674e36ca01..398ed45c19 100644 --- a/src/main/java/org/craftercms/studio/impl/v2/sync/SyncFromRepositoryTask.java +++ b/src/main/java/org/craftercms/studio/impl/v2/sync/SyncFromRepositoryTask.java @@ -20,6 +20,7 @@ import org.apache.commons.collections4.ListUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang3.Strings; import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; @@ -46,6 +47,7 @@ import org.craftercms.studio.api.v2.utils.DalUtils; import org.craftercms.studio.api.v2.utils.StudioConfiguration; import org.craftercms.studio.api.v2.utils.StudioUtils; +import org.craftercms.studio.impl.v1.util.ContentUtils; import org.craftercms.studio.impl.v2.utils.DependencyUtils; import org.craftercms.studio.impl.v2.utils.TimeUtils; import org.dom4j.Document; @@ -74,8 +76,8 @@ import static java.time.Instant.now; import static java.util.Comparator.comparing; import static java.util.Objects.isNull; -import static org.apache.commons.lang3.StringUtils.EMPTY; -import static org.apache.commons.lang3.StringUtils.isNotEmpty; +import static org.apache.commons.lang3.ArrayUtils.contains; +import static org.apache.commons.lang3.StringUtils.*; import static org.apache.commons.lang3.Strings.CS; import static org.craftercms.studio.api.v1.constant.DmConstants.*; import static org.craftercms.studio.api.v1.constant.StudioConstants.CONTENT_TYPE_FOLDER; @@ -84,6 +86,7 @@ import static org.craftercms.studio.api.v2.dal.AuditLog.createAuditLogEntry; import static org.craftercms.studio.api.v2.dal.AuditLogConstants.*; import static org.craftercms.studio.api.v2.dal.ItemState.*; +import static org.craftercms.studio.api.v2.dal.RepoOperation.Action.MOVE; import static org.craftercms.studio.api.v2.utils.StudioConfiguration.CONFIGURATION_PATH_PATTERNS; import static org.craftercms.studio.api.v2.utils.StudioConfiguration.REPO_SYNC_CANCELLED_PACKAGE_COMMENT; import static org.craftercms.studio.api.v2.utils.StudioUtils.getPublishPackageLockKey; @@ -97,7 +100,7 @@ public class SyncFromRepositoryTask implements ApplicationEventPublisherAware { private static final Logger logger = LoggerFactory.getLogger(SyncFromRepositoryTask.class); private static final String DEFAULT_CANCELLED_PACKAGE_COMMENT = "Cancelled because of conflicts with changes from repository sync process"; - private static final Set CREATED_PATH_ACTIONS = Set.of(RepoOperation.Action.CREATE, RepoOperation.Action.COPY, RepoOperation.Action.MOVE); + private static final Set CREATED_PATH_ACTIONS = Set.of(RepoOperation.Action.CREATE, RepoOperation.Action.COPY, MOVE); private static final String EMPTY_FILE_END = FILE_SEPARATOR + EMPTY_FILE; private final SitesService sitesService; @@ -399,12 +402,12 @@ private void syncDatabaseWithRepo(Site site, List repoOperationsD * Get the paths for the actions that created paths in the repo, i.e.: create, copy, move * Update or delete will not affect parent id updates */ - private List getCreatedPaths(List chunk) { - return chunk.stream() - .filter(repoOperation -> CREATED_PATH_ACTIONS.contains(repoOperation.getAction())) - .map(RepoOperation::getPath) - .filter(p -> !p.endsWith(EMPTY_FILE_END)) - .toList(); + protected List getCreatedPaths(List operations) { + return operations.stream() + .filter(op -> CREATED_PATH_ACTIONS.contains(op.getAction())) + .map(op -> MOVE.equals(op.getAction()) ? op.getMoveToPath() : op.getPath()) + .filter(p -> !p.endsWith(EMPTY_FILE_END)) + .toList(); } /** @@ -657,9 +660,11 @@ private void processUpdate(ItemDAO itemDao, DependencyDAO dependencyDao, SqlSess private void processMove(ItemDAO itemDao, DependencyDAO dependencyDao, SqlSession sqlSession, Site site, RepoOperation repoOperation, User user, Set allAncestors) throws SiteNotFoundException { - ItemMetadata metadata = getItemMetadata(site.getSiteId(), repoOperation.getMoveToPath()); - processAncestors(itemDao, site.getSiteId(), repoOperation.getMoveToPath(), user.getId(), - repoOperation.getDateTime(), allAncestors); + String oldPath = CS.removeEnd(repoOperation.getPath(), EMPTY_FILE_END); + String newPath = CS.removeEnd(repoOperation.getMoveToPath(), EMPTY_FILE_END); + ItemMetadata metadata = getItemMetadata(site.getSiteId(), newPath); + processAncestors(itemDao, site.getSiteId(), newPath, user.getId(), + repoOperation.getDateTime(), allAncestors); long onStateBitMap = SAVE_AND_CLOSE_ON_MASK; long offStateBitmap = SAVE_AND_CLOSE_OFF_MASK; if (metadata.disabled) { @@ -667,16 +672,17 @@ private void processMove(ItemDAO itemDao, DependencyDAO dependencyDao, SqlSessio } else { offStateBitmap = offStateBitmap | DISABLED.value; } - if (!ArrayUtils.contains(IGNORE_FILES, FilenameUtils.getName(repoOperation.getPath())) && - !ArrayUtils.contains(IGNORE_FILES, FilenameUtils.getName(repoOperation.getMoveToPath()))) { - itemDao.moveItemForSyncTask(site.getSiteId(), repoOperation.getPath(), repoOperation.getMoveToPath(), onStateBitMap, offStateBitmap); - updateItemRow(itemDao, site.getId(), - repoOperation.getPath(), metadata.previewUrl, onStateBitMap, offStateBitmap, user.getId(), - repoOperation.getDateTime(), metadata.label, metadata.contentTypeId, - contentService.getContentTypeClass(site.getSiteId(), repoOperation.getPath()), - StudioUtils.getMimeType(FilenameUtils.getName(repoOperation.getPath())), - contentRepository.getContentSize(site.getSiteId(), repoOperation.getPath())); + if (!ArrayUtils.contains(IGNORE_FILES, oldPath) && + !ArrayUtils.contains(IGNORE_FILES, newPath)) { + itemDao.moveItemForSyncTask(site.getSiteId(), oldPath, newPath, null, + metadata.previewUrl, metadata.label, user.getId(), + repoOperation.getDateTime().toInstant(),onStateBitMap, offStateBitmap, + metadata.contentTypeId, + contentService.getContentTypeClass(site.getSiteId(), newPath), + StudioUtils.getMimeType(FilenameUtils.getName(newPath)), + contentRepository.getContentSize(site.getSiteId(), newPath)); +// itemDao.moveItem(site.getSiteId(),oldPath, newPath, null, metadata.previewUrl,metadata.label, user.getId()); DependencyUtils.updateDependencies(site.getSiteId(), repoOperation.getMoveToPath(), repoOperation.getPath(), dependencyServiceInternal, dependencyDao, sqlSession); @@ -706,7 +712,7 @@ private void updateItemRow(ItemDAO itemDao, long siteId, String path, String pre Long size) { Timestamp sqlTsLastModified = new Timestamp(lastModifiedOn.toInstant().toEpochMilli()); String fileName = FilenameUtils.getName(path); - boolean ignored = org.apache.commons.lang3.ArrayUtils.contains(IGNORE_FILES, fileName); + boolean ignored = contains(IGNORE_FILES, fileName); itemDao.updateItemForSyncTask(siteId, path, previewUrl, onStatesBitMap, offStatesBitMap, lastModifiedBy, sqlTsLastModified.toString(), label, contentTypeId, systemType, mimeType, size, ignored); } diff --git a/src/main/resources/org/craftercms/studio/api/v2/dal/ItemDAO.xml b/src/main/resources/org/craftercms/studio/api/v2/dal/ItemDAO.xml index 6719d7e2ea..1baed8d47a 100644 --- a/src/main/resources/org/craftercms/studio/api/v2/dal/ItemDAO.xml +++ b/src/main/resources/org/craftercms/studio/api/v2/dal/ItemDAO.xml @@ -379,7 +379,19 @@ i.state = CASE WHEN i.system_type = '${systemTypeFolder}' THEN 0 ELSE (i.state | #{onStatesBitMap}) & ~#{offStatesBitMap} END, i.preview_url = IF(i.system_type = '${systemTypeFolder}', NULL, #{newPreviewUrl}), i.last_modified_by = #{userId}, - i.last_modified_on = NOW() + i.last_modified_on = #{lastModifiedOn} + + , i.content_type_id = #{contentTypeId} + + + , i.system_type = #{systemType}, + + + , i.mime_type = #{mimeType}, + + + , i.size = #{size} + , i.label = #{label} @@ -666,19 +678,19 @@ WHERE item.id = updates.childId; - - UPDATE item - SET - path = REPLACE(path, #{previousPath}, #{newPath}), - locked_by = NULL, - state = CASE - WHEN system_type = '${systemTypeFolder}' THEN 0 - ELSE (state | #{onStatesBitMap}) & ~#{offStatesBitMap} - END - WHERE - site_id = #{siteId} - AND (path = #{previousPath} OR path LIKE CONCAT(#{previousPath}, '/%')); - + + + + + + + + + + + + + UPDATE item diff --git a/src/test/java/org/craftercms/studio/impl/v2/sync/SyncFromRepositoryTaskTest.java b/src/test/java/org/craftercms/studio/impl/v2/sync/SyncFromRepositoryTaskTest.java new file mode 100644 index 0000000000..2a3d5eb6a6 --- /dev/null +++ b/src/test/java/org/craftercms/studio/impl/v2/sync/SyncFromRepositoryTaskTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2007-2025 Crafter Software Corporation. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as published by + * the Free Software Foundation. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.craftercms.studio.impl.v2.sync; + +import org.craftercms.studio.api.v2.dal.RepoOperation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@RunWith(MockitoJUnitRunner.class) +public class SyncFromRepositoryTaskTest { + + @InjectMocks + protected SyncFromRepositoryTask task; + + @Test + public void getCreatedPathsTest() { + RepoOperation op1 = new RepoOperation(RepoOperation.Action.CREATE, "/path1", + null, null, "123"); + RepoOperation op2 = new RepoOperation(RepoOperation.Action.MOVE, "/old-one", + null, "/new-one", "123"); + + List createdPaths = task.getCreatedPaths(List.of(op1, op2)); + assertEquals(createdPaths.size(), 2); + assertEquals(createdPaths.getFirst(), "/path1"); + assertEquals(createdPaths.getLast(), "/new-one"); + } +}