This is an automated email from the ASF dual-hosted git repository.

voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new 08af5869addb fix(cli): repair the hudi-cli tests and re-enable the 
module in CI (#18816)
08af5869addb is described below

commit 08af5869addbdf3249464cf8771f60150e0bd0d2
Author: Kavya Popat <[email protected]>
AuthorDate: Tue Sep 8 18:20:05 2026 +0530

    fix(cli): repair the hudi-cli tests and re-enable the module in CI (#18816)
    
    The hudi-cli tests have not run in CI since #10805 excluded the module.
    #11866 then repurposed the fifth parameter of TableCommand.createTable
    from --layoutVersion to --tableVersion. Both are Integer, so the 14 call
    sites passing TimelineLayoutVersion.VERSION_1 kept compiling and quietly
    created table-version-1 tables, which Hudi 1.x rejects.
    
    Tests: pass the current table version at those call sites, and fix the
    timeline path, file-name generator and sort order that the v9 layout
    changed. New coverage for archived clean and replacecommit rows, a v6
    table read over the legacy archive, an empty timeline under show fsview
    latest, and the clean-plan corruption cases below.
    
    RepairsCommand: repair corrupted clean files left empty .clean.requested
    files in place, because reading one raises "unable to read commit
    metadata" rather than the "Not an Avro data file" it matched. That
    message cannot be matched either, since the serde wraps a transient read
    failure in it too. The command now reads the plan bytes in full, so a
    read failure propagates and the instant stays, and only the in-memory
    decode judges corruption -- through any runtime failure, since Avro
    raises UnsupportedOperationException past its array ceiling and a
    versionless plan leaves the migrator unboxing a null. A corrupt plan now
    takes the whole pending action with it, inflight file first, instead of
    leaving the requested file behind for a second pass.
    
    ArchivedCommitsCommand: read through the archived timeline on the v8+
    LSM layout instead of globbing .commits_.archive*, uncached so that a
    trigger archival earlier in the session is visible. Tables below v8 keep
    the legacy reader, which now renders the completed entry of each instant
    and skips entries with no metadata, where a writer's empty requested
    file used to NPE it. Actions with no typed reader render through the
    schema embedded in their payload instead of failing the listing, and
    without --sortBy the printer keeps timeline order and cuts at --limit,
    so only the instants that can be rendered are loaded. Output change: the
    legacy format archived each state separately, so show archived commits
    printed three rows per commit and now prints one on both layouts.
    
    CleansCommand renders clean partitions in partition order rather than
    map iteration order, and FileSystemViewCommand guards the active
    timeline lookup so view commands do not throw on an empty timeline.
    
    CI: drop !hudi-cli from azure-pipelines-20230430.yml, and run the module
    inside test-spark-client-and-hadoop-common in bot.yml, since Codecov is
    fed only by bot.yml. That job's -am reactor grows from 10 to 23 modules
    and it runs about 43 min, roughly 30 min under the critical path.
    
    The hudi-cli functional suite goes from 113 tests with 12 failures and
    24 errors to 118 with none, all 20 classes green, and every HUDI-7614
    disable lifted.
    
    Closes #16448
---
 .github/workflows/bot.yml                          |  26 +-
 azure-pipelines-20230430.yml                       |   2 -
 .../hudi/cli/commands/ArchivedCommitsCommand.java  | 357 +++++++++++++----
 .../apache/hudi/cli/commands/CleansCommand.java    |   6 +-
 .../hudi/cli/commands/FileSystemViewCommand.java   |  13 +-
 .../apache/hudi/cli/commands/RepairsCommand.java   |  77 +++-
 .../org/apache/hudi/cli/commands/SparkMain.java    |   4 +-
 .../hudi/cli/commands/TestArchiveCommand.java      |  39 +-
 .../cli/commands/TestArchivedCommitsCommand.java   | 426 ++++++++++++++++++---
 .../hudi/cli/commands/TestCleansCommand.java       |   8 +-
 .../hudi/cli/commands/TestCommitsCommand.java      |  15 +-
 .../hudi/cli/commands/TestCompactionCommand.java   |  10 +-
 .../apache/hudi/cli/commands/TestDiffCommand.java  |   4 +-
 .../cli/commands/TestFileSystemViewCommand.java    |  62 ++-
 .../cli/commands/TestHoodieLogFileCommand.java     |   4 +-
 .../hudi/cli/commands/TestRepairsCommand.java      | 325 ++++++++++++++--
 .../hudi/cli/commands/TestRestoresCommand.java     |  85 ++--
 .../hudi/cli/commands/TestRollbacksCommand.java    |   4 +-
 .../hudi/cli/commands/TestSavepointsCommand.java   |   4 +-
 .../apache/hudi/cli/commands/TestStatsCommand.java |   4 +-
 .../cli/commands/TestUpgradeDowngradeCommand.java  |   3 +-
 21 files changed, 1177 insertions(+), 301 deletions(-)

diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml
index dceae0761bfb..1887ac7acbc2 100644
--- a/.github/workflows/bot.yml
+++ b/.github/workflows/bot.yml
@@ -114,10 +114,16 @@ jobs:
       - name: RAT check
         run: ./scripts/release/validate_source_rat.sh
 
+  # hudi-cli rides along with hudi-spark-client here rather than joining the 
catch-all
+  # module lists in test-common-and-other-modules: its functional suite starts 
a
+  # SparkSession per test class, and this job has the headroom for it while 
that one does
+  # not. hudi-cli's dependency closure is a superset of hudi-spark-client's, 
so it shares
+  # this job's build instead of paying for a second one. It stays listed in 
the catch-all
+  # job's UT_MODULES/FT_MODULES exclusions so the tests are not run twice.
   test-spark-client-and-hadoop-common:
     runs-on: ubuntu-latest
     needs: changes
-    timeout-minutes: 60
+    timeout-minutes: 75
     strategy:
       matrix:
         include:
@@ -143,7 +149,7 @@ jobs:
           SPARK_PROFILE: ${{ matrix.sparkProfile }}
           FLINK_PROFILE: ${{ matrix.flinkProfile }}
         run:
-          mvn clean install -T 2 -D"$SCALA_PROFILE" -D"$SPARK_PROFILE" 
-D"$FLINK_PROFILE" -DskipTests=true -Phudi-platform-service $MVN_ARGS -am -pl 
hudi-client/hudi-spark-client
+          mvn clean install -T 2 -D"$SCALA_PROFILE" -D"$SPARK_PROFILE" 
-D"$FLINK_PROFILE" -DskipTests=true -Phudi-platform-service $MVN_ARGS -am -pl 
hudi-client/hudi-spark-client,hudi-cli
       - name: UT - hudi-hadoop-common
         if: needs.changes.outputs.relevant == 'true'
         env:
@@ -168,6 +174,22 @@ jobs:
           FLINK_PROFILE: ${{ matrix.flinkProfile }}
         run:
           mvn test -Pfunctional-tests -D"$SCALA_PROFILE" -D"$SPARK_PROFILE" 
-D"$FLINK_PROFILE" -fae -pl hudi-client/hudi-spark-client $MVN_ARGS 
-Djacoco.skip=false
+      - name: UT - hudi-cli
+        if: needs.changes.outputs.relevant == 'true'
+        env:
+          SCALA_PROFILE: ${{ matrix.scalaProfile }}
+          SPARK_PROFILE: ${{ matrix.sparkProfile }}
+          FLINK_PROFILE: ${{ matrix.flinkProfile }}
+        run:
+          mvn test -Punit-tests -D"$SCALA_PROFILE" -D"$SPARK_PROFILE" 
-D"$FLINK_PROFILE" -fae -pl hudi-cli $MVN_ARGS -Djacoco.skip=false
+      - name: FT - hudi-cli
+        if: needs.changes.outputs.relevant == 'true'
+        env:
+          SCALA_PROFILE: ${{ matrix.scalaProfile }}
+          SPARK_PROFILE: ${{ matrix.sparkProfile }}
+          FLINK_PROFILE: ${{ matrix.flinkProfile }}
+        run:
+          mvn test -Pfunctional-tests -D"$SCALA_PROFILE" -D"$SPARK_PROFILE" 
-D"$FLINK_PROFILE" -fae -pl hudi-cli $MVN_ARGS -Djacoco.skip=false
       - name: Generate merged coverage report
         if: always() && needs.changes.outputs.relevant == 'true'
         run: ./scripts/jacoco/generate_merged_coverage_report.sh 
$GITHUB_WORKSPACE
diff --git a/azure-pipelines-20230430.yml b/azure-pipelines-20230430.yml
index b863f65bb1c5..0e3667a238d7 100644
--- a/azure-pipelines-20230430.yml
+++ b/azure-pipelines-20230430.yml
@@ -43,7 +43,6 @@ parameters:
       - '!hudi-hadoop-mr'
       - '!hudi-client/hudi-java-client'
       - '!hudi-client/hudi-spark-client'
-      - '!hudi-cli'
       - '!hudi-examples'
       - '!hudi-examples/hudi-examples-common'
       - '!hudi-examples/hudi-examples-flink'
@@ -59,7 +58,6 @@ parameters:
     type: object
     default:
       - '!hudi-client/hudi-spark-client'
-      - '!hudi-cli'
       - '!hudi-examples'
       - '!hudi-examples/hudi-examples-common'
       - '!hudi-examples/hudi-examples-flink'
diff --git 
a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/ArchivedCommitsCommand.java
 
b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/ArchivedCommitsCommand.java
index c2cbe30cd0b5..2a50ca0a586f 100644
--- 
a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/ArchivedCommitsCommand.java
+++ 
b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/ArchivedCommitsCommand.java
@@ -20,6 +20,7 @@ package org.apache.hudi.cli.commands;
 
 import org.apache.hudi.avro.model.HoodieArchivedMetaEntry;
 import org.apache.hudi.avro.model.HoodieCommitMetadata;
+import org.apache.hudi.avro.model.HoodieReplaceCommitMetadata;
 import org.apache.hudi.cli.HoodieCLI;
 import org.apache.hudi.cli.HoodiePrintHelper;
 import org.apache.hudi.cli.TableHeader;
@@ -27,22 +28,27 @@ import org.apache.hudi.cli.commands.SparkMain.SparkCommand;
 import org.apache.hudi.cli.utils.InputStreamConsumer;
 import org.apache.hudi.cli.utils.SparkUtil;
 import org.apache.hudi.common.model.HoodieLogFile;
-import org.apache.hudi.common.model.HoodieRecord;
 import org.apache.hudi.common.model.HoodieRecord.HoodieRecordType;
 import org.apache.hudi.common.schema.HoodieSchema;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
 import org.apache.hudi.common.table.log.HoodieLogFormat;
 import org.apache.hudi.common.table.log.HoodieLogFormat.Reader;
 import org.apache.hudi.common.table.log.block.HoodieAvroDataBlock;
+import org.apache.hudi.common.table.timeline.HoodieArchivedTimeline;
+import org.apache.hudi.common.table.timeline.HoodieInstant;
 import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
 import org.apache.hudi.common.util.Option;
-import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.common.util.StringUtils;
+import org.apache.hudi.common.util.VisibleForTesting;
 import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.storage.HoodieStorage;
 import org.apache.hudi.storage.StoragePath;
 import org.apache.hudi.storage.StoragePathInfo;
 
 import lombok.extern.slf4j.Slf4j;
+import org.apache.avro.file.DataFileStream;
+import org.apache.avro.generic.GenericDatumReader;
 import org.apache.avro.generic.GenericRecord;
 import org.apache.avro.generic.IndexedRecord;
 import org.apache.avro.specific.SpecificData;
@@ -52,11 +58,16 @@ import org.springframework.shell.standard.ShellComponent;
 import org.springframework.shell.standard.ShellMethod;
 import org.springframework.shell.standard.ShellOption;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import static 
org.apache.hudi.util.JavaScalaConverters.convertJavaPropertiesToScalaMap;
 
@@ -98,7 +109,12 @@ public class ArchivedCommitsCommand {
 
   @ShellMethod(key = "show archived commit stats", value = "Read commits from 
archived files and show file group details")
   public String showArchivedCommits(
-      @ShellOption(value = {"--archiveFolderPattern"}, help = "Archive 
Folder", defaultValue = "") String folder,
+      @ShellOption(value = {"--archiveFolderPattern"},
+          help = "Archive Folder, a folder under the meta path holding archive 
files in the"
+              + " legacy log format written before table version 8. When 
absent, a table"
+              + " written before table version 8 falls back to its default 
archive folder"
+              + " and any other table is read through its archived timeline",
+          defaultValue = "") String folder,
       @ShellOption(value = {"--limit"}, help = "Limit commits", defaultValue = 
"10") final Integer limit,
       @ShellOption(value = {"--sortBy"}, help = "Sorting Field", defaultValue 
= "") final String sortByField,
       @ShellOption(value = {"--desc"}, help = "Ordering", defaultValue = 
"false") final boolean descending,
@@ -107,9 +123,186 @@ public class ArchivedCommitsCommand {
       throws IOException {
     System.out.println("===============> Showing only " + limit + " archived 
commits <===============");
     HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
-    StoragePath archivePath = folder != null && !folder.isEmpty()
-        ? new StoragePath(metaClient.getMetaPath(), folder)
-        : new StoragePath(metaClient.getArchivePath(), ".commits_.archive*");
+    List<Comparable[]> allStats;
+    if (folder != null && !folder.isEmpty()) {
+      allStats = readCommitStatsFromLegacyArchive(metaClient, new 
StoragePath(metaClient.getMetaPath(), folder));
+    } else if (isLegacyArchive(metaClient)) {
+      allStats = readCommitStatsFromLegacyArchive(
+          metaClient, new StoragePath(metaClient.getArchivePath(), 
".commits_.archive*"));
+    } else {
+      allStats = 
readCommitStatsFromArchivedTimeline(newArchivedTimeline(metaClient), 
sortByField, limit);
+    }
+    TableHeader header = new 
TableHeader().addTableHeaderField("action").addTableHeaderField("instant")
+        
.addTableHeaderField("partition").addTableHeaderField("file_id").addTableHeaderField("prev_instant")
+        
.addTableHeaderField("num_writes").addTableHeaderField("num_inserts").addTableHeaderField("num_deletes")
+        
.addTableHeaderField("num_update_writes").addTableHeaderField("total_log_files")
+        
.addTableHeaderField("total_log_blocks").addTableHeaderField("total_corrupt_log_blocks")
+        
.addTableHeaderField("total_rollback_blocks").addTableHeaderField("total_log_records")
+        
.addTableHeaderField("total_updated_records_compacted").addTableHeaderField("total_write_bytes")
+        .addTableHeaderField("total_write_errors");
+
+    return HoodiePrintHelper.print(header, new HashMap<>(), sortByField, 
descending, limit, headerOnly, allStats);
+  }
+
+  @ShellMethod(key = "show archived commits", value = "Read commits from 
archived files and show details")
+  public String showCommits(
+      @ShellOption(value = {"--skipMetadata"}, help = "Skip displaying commit 
metadata",
+          defaultValue = "true") boolean skipMetadata,
+      @ShellOption(value = {"--limit"}, help = "Limit commits", defaultValue = 
"10") final Integer limit,
+      @ShellOption(value = {"--sortBy"}, help = "Sorting Field", defaultValue 
= "") final String sortByField,
+      @ShellOption(value = {"--desc"}, help = "Ordering", defaultValue = 
"false") final boolean descending,
+      @ShellOption(value = {"--headeronly"}, help = "Print Header Only",
+              defaultValue = "false") final boolean headerOnly) {
+
+    System.out.println("===============> Showing only " + limit + " archived 
commits <===============");
+    HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
+    List<Comparable[]> allCommits = readArchivedCommits(
+        newArchivedTimeline(metaClient), skipMetadata, 
isLegacyArchive(metaClient), sortByField, limit);
+
+    TableHeader header = new 
TableHeader().addTableHeaderField("CommitTime").addTableHeaderField("CommitType");
+
+    if (!skipMetadata) {
+      header = header.addTableHeaderField("CommitDetails");
+    }
+
+    return HoodiePrintHelper.print(header, new HashMap<>(), sortByField, 
descending, limit, headerOnly, allCommits);
+  }
+
+  /**
+   * Renders the completed archived instants as rows, loading metadata only 
for the rows that
+   * can reach the output.
+   * <p>
+   * Without a sort field the printer keeps the timeline order and cuts at the 
limit, so only
+   * the leading instants can be shown, and an archive that has grown for 
years holds far more
+   * payload than those few rows need. With a sort field, or no limit, every 
row takes part
+   * and everything has to be loaded.
+   */
+  @VisibleForTesting
+  static List<Comparable[]> readArchivedCommits(HoodieArchivedTimeline 
archivedTimeline, boolean skipMetadata,
+                                                boolean legacyArchive, String 
sortByField, int limit) {
+    List<HoodieInstant> completed = archivedTimeline.getInstants().stream()
+        .filter(HoodieInstant::isCompleted)
+        .collect(Collectors.toList());
+    List<HoodieInstant> shown = boundedByLimit(completed, sortByField, limit);
+    if (!skipMetadata) {
+      loadInstantDetails(archivedTimeline, shown, shown.size() < 
completed.size());
+    }
+    return shown.stream()
+        .map(instant -> readArchivedCommit(archivedTimeline, instant, 
skipMetadata, legacyArchive))
+        .collect(Collectors.toList());
+  }
+
+  /**
+   * Returns the leading instants that the printer can render, or all of them 
when a sort field
+   * or a non-positive limit makes every row a candidate.
+   */
+  private static List<HoodieInstant> boundedByLimit(List<HoodieInstant> 
instants, String sortByField, int limit) {
+    if (!sortByField.isEmpty() || limit <= 0 || limit >= instants.size()) {
+      return instants;
+    }
+    return instants.subList(0, limit);
+  }
+
+  /**
+   * Loads the details of the given instants, through the closed time range 
they span when they
+   * are a strict subset of the archive, so that the payloads outside the 
range are never held.
+   * <p>
+   * On the LSM timeline that table version 8 and above are written with, the 
range prunes the
+   * archive files by the instant range in their names, so the files outside 
it are never opened.
+   * The legacy layout carries no such range in its file names and reads every 
archive file
+   * either way, keeping only the records that fall in the range.
+   */
+  private static void loadInstantDetails(HoodieArchivedTimeline 
archivedTimeline, List<HoodieInstant> instants,
+                                         boolean subset) {
+    if (instants.isEmpty()) {
+      return;
+    }
+    if (subset) {
+      archivedTimeline.loadCompletedInstantDetailsInMemory(
+          instants.get(0).requestedTime(), instants.get(instants.size() - 
1).requestedTime());
+    } else {
+      archivedTimeline.loadCompletedInstantDetailsInMemory();
+    }
+  }
+
+  /**
+   * Reads the write stats of the archived commit and delta commit instants 
through the
+   * archived timeline, the LSM timeline that table version 8 and above are 
written with.
+   * <p>
+   * Without a sort field the printer keeps the timeline order and cuts at the 
limit, so the
+   * rows come from a leading run of the write instants. Each of them 
contributes a row per
+   * write stat, so their details are loaded a limit-sized window of instants 
at a time until
+   * the rows reach the limit, instead of materializing every archived 
payload. With a sort
+   * field, or no limit, every row takes part and everything has to be loaded.
+   */
+  @VisibleForTesting
+  static List<Comparable[]> 
readCommitStatsFromArchivedTimeline(HoodieArchivedTimeline archivedTimeline,
+                                                                String 
sortByField, int limit) {
+    List<HoodieInstant> writes = archivedTimeline.getInstants().stream()
+        .filter(HoodieInstant::isCompleted)
+        .filter(instant -> 
HoodieTimeline.COMMIT_ACTION.equals(instant.getAction())
+            || HoodieTimeline.DELTA_COMMIT_ACTION.equals(instant.getAction()))
+        .collect(Collectors.toList());
+    if (!sortByField.isEmpty() || limit <= 0) {
+      archivedTimeline.loadCompletedInstantDetailsInMemory();
+      return writes.stream()
+          .flatMap(instant -> readWriteStatRows(archivedTimeline, instant))
+          .collect(Collectors.toList());
+    }
+    List<Comparable[]> rows = new ArrayList<>();
+    for (int from = 0; from < writes.size() && rows.size() < limit; from += 
limit) {
+      List<HoodieInstant> window = writes.subList(from, Math.min(from + limit, 
writes.size()));
+      loadInstantDetails(archivedTimeline, window, window.size() < 
writes.size());
+      window.forEach(instant -> readWriteStatRows(archivedTimeline, 
instant).forEach(rows::add));
+    }
+    return rows;
+  }
+
+  /**
+   * Returns whether the table keeps its archived instants in the legacy log 
format, the archive
+   * layout of the timeline layout version 1 that table versions before eight 
are written with.
+   */
+  private static boolean isLegacyArchive(HoodieTableMetaClient metaClient) {
+    return 
metaClient.getTimelineLayoutVersion().compareTo(TimelineLayoutVersion.LAYOUT_VERSION_2)
 < 0;
+  }
+
+  /**
+   * Builds an archived timeline that bypasses the meta client cache, so that 
an archival
+   * triggered earlier in the same CLI session is visible. The instance is 
discarded with the
+   * command, so the instant details it loads need no explicit eviction.
+   */
+  private static HoodieArchivedTimeline 
newArchivedTimeline(HoodieTableMetaClient metaClient) {
+    return metaClient.getArchivedTimeline(StringUtils.EMPTY_STRING, false);
+  }
+
+  private static Stream<Comparable[]> readWriteStatRows(HoodieArchivedTimeline 
archivedTimeline, HoodieInstant instant) {
+    HoodieCommitMetadata metadata;
+    try {
+      metadata = archivedTimeline.readCommitMetadataToAvro(instant);
+    } catch (IOException e) {
+      throw new HoodieException("Failed to read the archived commit metadata 
of instant " + instant, e);
+    }
+    if (metadata == null || metadata.getPartitionToWriteStats() == null) {
+      return Stream.empty();
+    }
+    final String action = instant.getAction();
+    final String instantTime = instant.requestedTime();
+    return sortByKey(metadata.getPartitionToWriteStats()).values().stream()
+        .flatMap(List::stream)
+        .map(writeStat -> new Comparable[] {action, instantTime, 
writeStat.getPartitionPath(),
+            writeStat.getFileId(), writeStat.getPrevCommit(), 
writeStat.getNumWrites(),
+            writeStat.getNumInserts(), writeStat.getNumDeletes(), 
writeStat.getNumUpdateWrites(),
+            writeStat.getTotalLogFiles(), writeStat.getTotalLogBlocks(), 
writeStat.getTotalCorruptLogBlock(),
+            writeStat.getTotalRollbackBlocks(), writeStat.getTotalLogRecords(),
+            writeStat.getTotalUpdatedRecordsCompacted(), 
writeStat.getTotalWriteBytes(),
+            writeStat.getTotalWriteErrors()});
+  }
+
+  /**
+   * Reads the write stats of the archived commit and delta commit instants 
from the given
+   * folder of archive files in the legacy log format written before table 
version 8.
+   */
+  private List<Comparable[]> 
readCommitStatsFromLegacyArchive(HoodieTableMetaClient metaClient, StoragePath 
archivePath) throws IOException {
     HoodieStorage storage = metaClient.getStorage();
     List<StoragePathInfo> pathInfoList = storage.globEntries(archivePath);
     List<Comparable[]> allStats = new ArrayList<>();
@@ -126,12 +319,15 @@ public class ArchivedCommitsCommand {
         List<Comparable[]> readCommits = readRecords.stream().map(r -> 
(GenericRecord) r)
             .filter(r -> 
r.get("actionType").toString().equals(HoodieTimeline.COMMIT_ACTION)
                 || 
r.get("actionType").toString().equals(HoodieTimeline.DELTA_COMMIT_ACTION))
+            // the legacy archive holds an entry per instant state; only the 
completed one carries
+            // the final write stats, and the requested entry of a writer 
carries no metadata at all
+            .filter(r -> isCompletedEntry(r) && r.get("hoodieCommitMetadata") 
!= null)
             .flatMap(r -> {
               HoodieCommitMetadata metadata = (HoodieCommitMetadata) 
SpecificData.get()
                   .deepCopy(HoodieCommitMetadata.SCHEMA$, 
r.get("hoodieCommitMetadata"));
               final String instantTime = r.get("commitTime").toString();
               final String action = r.get("actionType").toString();
-              return 
metadata.getPartitionToWriteStats().values().stream().flatMap(hoodieWriteStats 
-> hoodieWriteStats.stream().map(hoodieWriteStat -> {
+              return 
sortByKey(metadata.getPartitionToWriteStats()).values().stream().flatMap(hoodieWriteStats
 -> hoodieWriteStats.stream().map(hoodieWriteStat -> {
                 List<Comparable> row = new ArrayList<>();
                 row.add(action);
                 row.add(instantTime);
@@ -156,94 +352,89 @@ public class ArchivedCommitsCommand {
         allStats.addAll(readCommits);
       }
     }
-    TableHeader header = new 
TableHeader().addTableHeaderField("action").addTableHeaderField("instant")
-        
.addTableHeaderField("partition").addTableHeaderField("file_id").addTableHeaderField("prev_instant")
-        
.addTableHeaderField("num_writes").addTableHeaderField("num_inserts").addTableHeaderField("num_deletes")
-        
.addTableHeaderField("num_update_writes").addTableHeaderField("total_log_files")
-        
.addTableHeaderField("total_log_blocks").addTableHeaderField("total_corrupt_log_blocks")
-        
.addTableHeaderField("total_rollback_blocks").addTableHeaderField("total_log_records")
-        
.addTableHeaderField("total_updated_records_compacted").addTableHeaderField("total_write_bytes")
-        .addTableHeaderField("total_write_errors");
+    return allStats;
+  }
 
-    return HoodiePrintHelper.print(header, new HashMap<>(), sortByField, 
descending, limit, headerOnly, allStats);
+  private static boolean isCompletedEntry(GenericRecord archivedEntry) {
+    // entries written before the state was recorded are completed ones
+    Object actionState = archivedEntry.get("actionState");
+    return actionState == null || 
HoodieInstant.State.COMPLETED.name().equals(actionState.toString());
   }
 
-  @ShellMethod(key = "show archived commits", value = "Read commits from 
archived files and show details")
-  public String showCommits(
-      @ShellOption(value = {"--skipMetadata"}, help = "Skip displaying commit 
metadata",
-          defaultValue = "true") boolean skipMetadata,
-      @ShellOption(value = {"--limit"}, help = "Limit commits", defaultValue = 
"10") final Integer limit,
-      @ShellOption(value = {"--sortBy"}, help = "Sorting Field", defaultValue 
= "") final String sortByField,
-      @ShellOption(value = {"--desc"}, help = "Ordering", defaultValue = 
"false") final boolean descending,
-      @ShellOption(value = {"--headeronly"}, help = "Print Header Only",
-              defaultValue = "false") final boolean headerOnly)
-      throws IOException {
+  private static Comparable[] readArchivedCommit(HoodieArchivedTimeline 
archivedTimeline, HoodieInstant instant,
+                                                 boolean skipMetadata, boolean 
legacyArchive) {
+    List<Comparable> commitDetails = new ArrayList<>();
+    commitDetails.add(instant.requestedTime());
+    commitDetails.add(instant.getAction());
+    if (!skipMetadata) {
+      commitDetails.add(readArchivedMetadataString(archivedTimeline, instant, 
legacyArchive));
+    }
+    return commitDetails.toArray(new Comparable[commitDetails.size()]);
+  }
 
-    System.out.println("===============> Showing only " + limit + " archived 
commits <===============");
-    HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
-    StoragePath archivePath =
-        new StoragePath(metaClient.getArchivePath(), ".commits_.archive*");
-    HoodieStorage storage = metaClient.getStorage();
-    List<StoragePathInfo> pathInfoList = storage.globEntries(archivePath);
-    List<Comparable[]> allCommits = new ArrayList<>();
-    for (StoragePathInfo pathInfo : pathInfoList) {
-      // read the archived file
-      try (HoodieLogFormat.Reader reader = 
HoodieLogFormat.newReader(metaClient,
-          new HoodieLogFile(pathInfo.getPath()), 
HoodieSchema.fromAvroSchema(HoodieArchivedMetaEntry.getClassSchema()))) {
-        List<IndexedRecord> readRecords = new ArrayList<>();
-        // read the avro blocks
-        while (reader.hasNext()) {
-          HoodieAvroDataBlock blk = (HoodieAvroDataBlock) reader.next();
-          try (ClosableIterator<HoodieRecord<IndexedRecord>> recordItr = 
blk.getRecordIterator(HoodieRecordType.AVRO)) {
-            recordItr.forEachRemaining(r -> readRecords.add(r.getData()));
-          }
-        }
-        List<Comparable[]> readCommits = readRecords.stream().map(r -> 
(GenericRecord) r)
-            .map(r -> readCommit(r, 
skipMetadata)).collect(Collectors.toList());
-        allCommits.addAll(readCommits);
+  private static String readArchivedMetadataString(HoodieArchivedTimeline 
archivedTimeline, HoodieInstant instant,
+                                                   boolean legacyArchive) {
+    Option<byte[]> details = archivedTimeline.getInstantDetails(instant);
+    if (!details.isPresent() || details.get().length == 0) {
+      // instants can be archived with no metadata, e.g. from an empty 
completed
+      // meta file that a writer failure left behind
+      return "{}";
+    }
+    if (legacyArchive) {
+      // ArchivedTimelineV1 caches the JSON rendering of each archived entry, 
which is what the
+      // legacy reader printed, and the v1 serde cannot decode it back into 
the Avro classes
+      return new String(details.get(), StandardCharsets.UTF_8);
+    }
+    try {
+      // TimelineArchiverV2 writes only the actions with a typed reader below, 
but the upgrade to the
+      // LSM timeline copies every entry of a legacy archive across, 
savepoints, compaction plans and
+      // index metadata included, and takes an entry written before the action 
state was recorded as
+      // completed. Every such payload is an Avro file that carries its own 
schema
+      switch (instant.getAction()) {
+        case HoodieTimeline.CLEAN_ACTION:
+          return archivedTimeline.readCleanMetadata(instant).toString();
+        case HoodieTimeline.COMMIT_ACTION:
+        case HoodieTimeline.DELTA_COMMIT_ACTION:
+          return 
sortPartitions(archivedTimeline.readCommitMetadataToAvro(instant)).toString();
+        case HoodieTimeline.ROLLBACK_ACTION:
+          return archivedTimeline.readRollbackMetadata(instant).toString();
+        case HoodieTimeline.REPLACE_COMMIT_ACTION:
+          return 
sortPartitions(archivedTimeline.readReplaceCommitMetadataToAvro(instant)).toString();
+        default:
+          return readAvroPayload(details.get());
       }
+    } catch (IOException e) {
+      throw new HoodieException("Failed to read the archived metadata of 
instant " + instant, e);
     }
+  }
 
-    TableHeader header = new 
TableHeader().addTableHeaderField("CommitTime").addTableHeaderField("CommitType");
-
-    if (!skipMetadata) {
-      header = header.addTableHeaderField("CommitDetails");
+  /**
+   * Renders an archived payload through the schema embedded in it, which is 
the rendering the
+   * typed Avro records give of themselves.
+   */
+  private static String readAvroPayload(byte[] payload) throws IOException {
+    try (DataFileStream<GenericRecord> reader =
+             new DataFileStream<>(new ByteArrayInputStream(payload), new 
GenericDatumReader<>())) {
+      return reader.hasNext() ? reader.next().toString() : "{}";
     }
-
-    return HoodiePrintHelper.print(header, new HashMap<>(), sortByField, 
descending, limit, headerOnly, allCommits);
   }
 
-  private Comparable[] commitDetail(GenericRecord record, String metadataName, 
boolean skipMetadata) {
-    List<Object> commitDetails = new ArrayList<>();
-    commitDetails.add(record.get("commitTime"));
-    commitDetails.add(record.get("actionType").toString());
-    if (!skipMetadata) {
-      
commitDetails.add(Option.ofNullable(record.get(metadataName)).orElse("{}").toString());
-    }
-    return commitDetails.toArray(new Comparable[commitDetails.size()]);
+  /**
+   * Orders the partition keyed maps of the metadata so that the rendering 
does not
+   * depend on the iteration order of the map the Avro decoder happens to 
build.
+   */
+  private static HoodieCommitMetadata sortPartitions(HoodieCommitMetadata 
metadata) {
+    
metadata.setPartitionToWriteStats(sortByKey(metadata.getPartitionToWriteStats()));
+    return metadata;
   }
 
-  private Comparable[] readCommit(GenericRecord record, boolean skipMetadata) {
-    String actionType = record.get("actionType").toString();
-    switch (actionType) {
-      case HoodieTimeline.CLEAN_ACTION:
-        return commitDetail(record, "hoodieCleanMetadata", skipMetadata);
-      case HoodieTimeline.COMMIT_ACTION:
-      case HoodieTimeline.DELTA_COMMIT_ACTION:
-        return commitDetail(record, "hoodieCommitMetadata", skipMetadata);
-      case HoodieTimeline.ROLLBACK_ACTION:
-        return commitDetail(record, "hoodieRollbackMetadata", skipMetadata);
-      case HoodieTimeline.SAVEPOINT_ACTION:
-        return commitDetail(record, "hoodieSavePointMetadata", skipMetadata);
-      case HoodieTimeline.COMPACTION_ACTION:
-        return commitDetail(record, "hoodieCompactionMetadata", skipMetadata);
-      case HoodieTimeline.REPLACE_COMMIT_ACTION:
-      case HoodieTimeline.CLUSTERING_ACTION:
-        return commitDetail(record, "hoodieReplaceCommitMetadata", 
skipMetadata);
-      default: {
-        throw new HoodieException("Unexpected action type: " + actionType);
-      }
-    }
+  private static HoodieReplaceCommitMetadata 
sortPartitions(HoodieReplaceCommitMetadata metadata) {
+    
metadata.setPartitionToWriteStats(sortByKey(metadata.getPartitionToWriteStats()));
+    
metadata.setPartitionToReplaceFileIds(sortByKey(metadata.getPartitionToReplaceFileIds()));
+    return metadata;
   }
 
+  private static <T> Map<String, T> sortByKey(Map<String, T> 
partitionKeyedMap) {
+    return partitionKeyedMap == null ? null : new TreeMap<>(partitionKeyedMap);
+  }
 }
diff --git 
a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/CleansCommand.java 
b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/CleansCommand.java
index 704e11719791..519f56236863 100644
--- a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/CleansCommand.java
+++ b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/CleansCommand.java
@@ -45,6 +45,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.stream.Collectors;
 
 import scala.collection.JavaConverters;
@@ -108,7 +109,10 @@ public class CleansCommand {
 
     HoodieCleanMetadata cleanMetadata = 
timeline.readCleanMetadata(cleanInstant);
     List<Comparable[]> rows = new ArrayList<>();
-    for (Map.Entry<String, HoodieCleanPartitionMetadata> entry : 
cleanMetadata.getPartitionMetadata().entrySet()) {
+    // the metadata's partition map carries no order of its own, and the 
printer sorts only when
+    // --sortBy is given, so the rows are keyed in partition order to keep the 
output stable
+    for (Map.Entry<String, HoodieCleanPartitionMetadata> entry :
+        new TreeMap<>(cleanMetadata.getPartitionMetadata()).entrySet()) {
       String path = entry.getKey();
       HoodieCleanPartitionMetadata stats = entry.getValue();
       String policy = stats.getPolicy();
diff --git 
a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/FileSystemViewCommand.java
 
b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/FileSystemViewCommand.java
index 1f3fbb51d9e6..28fca03f0558 100644
--- 
a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/FileSystemViewCommand.java
+++ 
b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/FileSystemViewCommand.java
@@ -31,6 +31,7 @@ import org.apache.hudi.common.table.timeline.HoodieTimeline;
 import org.apache.hudi.common.table.timeline.TimelineFactory;
 import org.apache.hudi.common.table.view.HoodieTableFileSystemView;
 import org.apache.hudi.common.util.NumericUtils;
+import org.apache.hudi.common.util.Option;
 import org.apache.hudi.storage.HoodieStorage;
 import org.apache.hudi.storage.StoragePath;
 import org.apache.hudi.storage.StoragePathInfo;
@@ -150,10 +151,16 @@ public class FileSystemViewCommand {
       fileSliceStream = fsView.getLatestFileSlices(partition);
     } else {
       if (maxInstant.isEmpty()) {
-        maxInstant = 
HoodieCLI.getTableMetaClient().getActiveTimeline().filterCompletedAndCompactionInstants().lastInstant()
-            .get().requestedTime();
+        Option<HoodieInstant> lastInstant = 
HoodieCLI.getTableMetaClient().getActiveTimeline().filterCompletedAndCompactionInstants().lastInstant();
+        if (lastInstant.isPresent()) {
+          maxInstant = lastInstant.get().requestedTime();
+        }
+      }
+      if (maxInstant.isEmpty()) {
+        fileSliceStream = fsView.getLatestFileSlices(partition);
+      } else {
+        fileSliceStream = 
fsView.getLatestMergedFileSlicesBeforeOrOn(partition, maxInstant);
       }
-      fileSliceStream = fsView.getLatestMergedFileSlicesBeforeOrOn(partition, 
maxInstant);
     }
 
     fileSliceStream.forEach(fs -> {
diff --git 
a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/RepairsCommand.java 
b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/RepairsCommand.java
index 872ff342dce5..5f7bb2525485 100644
--- a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/RepairsCommand.java
+++ b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/RepairsCommand.java
@@ -29,6 +29,7 @@ import org.apache.hudi.common.model.HoodiePartitionMetadata;
 import org.apache.hudi.common.table.HoodieTableConfig;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
 import org.apache.hudi.common.table.timeline.HoodieActiveTimeline;
+import org.apache.hudi.common.table.timeline.HoodieInstant;
 import org.apache.hudi.common.table.timeline.HoodieTimeline;
 import org.apache.hudi.common.table.timeline.TimelineUtils;
 import org.apache.hudi.common.util.CleanerUtils;
@@ -36,10 +37,10 @@ import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.PartitionPathEncodeUtils;
 import org.apache.hudi.common.util.StringUtils;
 import org.apache.hudi.exception.HoodieIOException;
+import org.apache.hudi.io.util.FileIOUtils;
 import org.apache.hudi.storage.StoragePath;
 
 import lombok.extern.slf4j.Slf4j;
-import org.apache.avro.AvroRuntimeException;
 import org.apache.spark.launcher.SparkLauncher;
 import org.apache.spark.sql.hudi.DeDupeType;
 import org.apache.spark.util.Utils;
@@ -47,8 +48,10 @@ import org.springframework.shell.standard.ShellComponent;
 import org.springframework.shell.standard.ShellMethod;
 import org.springframework.shell.standard.ShellOption;
 
+import java.io.ByteArrayInputStream;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
@@ -189,29 +192,71 @@ public class RepairsCommand {
 
   @ShellMethod(key = "repair corrupted clean files", value = "repair corrupted 
clean files")
   public void removeCorruptedPendingCleanAction() {
+    removeCorruptedPendingCleanAction(HoodieCLI.getTableMetaClient());
+  }
 
-    HoodieTableMetaClient client = HoodieCLI.getTableMetaClient();
-    HoodieTimeline cleanerTimeline = 
HoodieCLI.getTableMetaClient().getActiveTimeline().getCleanerTimeline();
+  /**
+   * Removes the pending clean instants whose plan is verifiably empty or 
corrupt.
+   * <p>
+   * The plan bytes are read in full before anything is judged. The timeline 
serde wraps every
+   * exception raised while it streams an instant file, a transient read 
failure included, in
+   * the same "unable to read commit metadata" IOException that an empty or 
truncated file
+   * raises, so the message cannot tell a storage outage from corruption. A 
failure of the
+   * read itself is therefore propagated, and only the in-memory decode, which 
no I/O can
+   * disturb, decides that the plan is corrupt.
+   */
+  static void removeCorruptedPendingCleanAction(HoodieTableMetaClient client) {
+    HoodieActiveTimeline activeTimeline = client.getActiveTimeline();
+    HoodieTimeline cleanerTimeline = activeTimeline.getCleanerTimeline();
     log.info("Inspecting pending clean metadata in timeline for corrupted 
files");
     
cleanerTimeline.filterInflightsAndRequested().getInstants().forEach(instant -> {
-      try {
-        CleanerUtils.getCleanerPlan(client, instant);
-      } catch (AvroRuntimeException e) {
-        log.warn("Corruption found. Trying to remove corrupted clean instant 
file: {}", instant);
+      HoodieInstant planInstant = CleanerUtils.getCleanRequestInstant(client, 
instant);
+      byte[] plan;
+      try (InputStream in = 
activeTimeline.getInstantContentStream(planInstant)) {
+        plan = FileIOUtils.readAsByteArray(in);
+      } catch (IOException e) {
+        throw new HoodieIOException("Failed to read the plan of pending clean 
instant " + instant, e);
+      }
+      if (plan.length > 0 && isReadableCleanerPlan(client, plan)) {
+        return;
+      }
+      log.warn("Corruption found. Trying to remove corrupted clean instant 
file: {}", instant);
+      // An inflight clean keeps its plan in the requested file, so removing 
only the instant the
+      // timeline listed would leave the corrupt plan behind for the next 
clean to fail on. The
+      // inflight file goes first: a failure between the two deletes leaves 
the action requested,
+      // which is a state this command already handles, whereas the reverse 
order would leave an
+      // inflight action with no plan at all.
+      TimelineUtils.deleteInstantFile(client.getStorage(), 
client.getTimelinePath(),
+          instant, client.getInstantFileNameGenerator());
+      if (!instant.equals(planInstant)) {
         TimelineUtils.deleteInstantFile(client.getStorage(), 
client.getTimelinePath(),
-            instant, client.getInstantFileNameGenerator());
-      } catch (IOException ioe) {
-        if (ioe.getMessage().contains("Not an Avro data file")) {
-          log.warn("Corruption found. Trying to remove corrupted clean instant 
file: {}", instant);
-          TimelineUtils.deleteInstantFile(client.getStorage(), 
client.getTimelinePath(),
-              instant, client.getInstantFileNameGenerator());
-        } else {
-          throw new HoodieIOException(ioe.getMessage(), ioe);
-        }
+            planInstant, client.getInstantFileNameGenerator());
       }
     });
   }
 
+  /**
+   * Decodes a clean plan held in memory, which fails only on the content 
itself: bytes that are not
+   * an Avro file or that stop short fail the read, and an Avro container that 
holds no record, which
+   * a writer killed between opening and closing it leaves behind, fails the 
serde's argument check.
+   * <p>
+   * Every failure is caught, because corrupt bytes do not reach the decoder 
through one exception
+   * type: a length that survives as far as Avro's own ceiling raises an {@code
+   * UnsupportedOperationException} rather than an {@code 
AvroRuntimeException}, and a plan that
+   * decodes with no version leaves the migrator to unbox a null. Nothing 
inside the try touches
+   * storage -- the bytes are already in memory and both clean plan migration 
handlers are pure
+   * transforms -- so there is no I/O failure here to mistake for corruption, 
and letting one of
+   * these escape would abandon the repair of every instant behind this one.
+   */
+  private static boolean isReadableCleanerPlan(HoodieTableMetaClient client, 
byte[] plan) {
+    try {
+      CleanerUtils.getCleanerPlan(client, new ByteArrayInputStream(plan));
+      return true;
+    } catch (IOException | RuntimeException e) {
+      return false;
+    }
+  }
+
   @ShellMethod(key = "repair show empty commit metadata", value = "show failed 
commits")
   public void showFailedCommits() {
     HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
diff --git a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/SparkMain.java 
b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/SparkMain.java
index 9f1c94af6026..121c1f8b4231 100644
--- a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/SparkMain.java
+++ b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/SparkMain.java
@@ -37,6 +37,7 @@ import org.apache.hudi.common.util.HoodieStorageUtils;
 import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.PartitionPathEncodeUtils;
 import org.apache.hudi.common.util.ValidationUtils;
+import org.apache.hudi.common.util.VisibleForTesting;
 import org.apache.hudi.config.HoodieBootstrapConfig;
 import org.apache.hudi.config.HoodieCleanConfig;
 import org.apache.hudi.config.HoodieIndexConfig;
@@ -623,7 +624,8 @@ public class SparkMain {
     }
   }
 
-  private static int archive(JavaSparkContext jsc, int minCommits, int 
maxCommits, int commitsRetained, boolean enableMetadata, String basePath) {
+  @VisibleForTesting
+  protected static int archive(JavaSparkContext jsc, int minCommits, int 
maxCommits, int commitsRetained, boolean enableMetadata, String basePath) {
     try {
       return ArchiveExecutorUtils.archive(jsc, minCommits, maxCommits, 
commitsRetained, enableMetadata, basePath, new HashMap<>());
     } catch (IOException ex) {
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestArchiveCommand.java 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestArchiveCommand.java
index 209d3744dfcd..6c2d829708c5 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestArchiveCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestArchiveCommand.java
@@ -21,25 +21,17 @@ package org.apache.hudi.cli.commands;
 import org.apache.hudi.cli.HoodieCLI;
 import org.apache.hudi.cli.functional.CLIFunctionalTestHarness;
 import org.apache.hudi.cli.testutils.HoodieTestCommitMetadataGenerator;
-import org.apache.hudi.cli.testutils.ShellEvaluationResultUtil;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.HoodieTableVersion;
 
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.shell.Shell;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @Tag("functional")
-@SpringBootTest(properties = {"spring.shell.interactive.enabled=false", 
"spring.shell.command.script.enabled=false"})
 public class TestArchiveCommand extends CLIFunctionalTestHarness {
 
-  @Autowired
-  private Shell shell;
-
   @Test
   public void testArchiving() throws Exception {
     HoodieCLI.conf = storageConf();
@@ -50,7 +42,7 @@ public class TestArchiveCommand extends 
CLIFunctionalTestHarness {
 
     new TableCommand().createTable(
         tablePath, tableName,
-        "COPY_ON_WRITE", "", 1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "COPY_ON_WRITE", "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
 
     HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
 
@@ -60,20 +52,27 @@ public class TestArchiveCommand extends 
CLIFunctionalTestHarness {
       
HoodieTestCommitMetadataGenerator.createCommitFileWithMetadata(tablePath, 
timestamp, storageConf());
     }
 
-    Object cmdResult = shell.evaluate(() -> "trigger archival --minCommits 2 
--maxCommits 3 --commitsRetainedByCleaner 1 --enableMetadata false");
-    assertTrue(ShellEvaluationResultUtil.isSuccess(cmdResult));
+    // The shell command "trigger archival" launches SparkMain in a separate 
spark-submit
+    // process, which needs SPARK_HOME and the jars packaged under target/lib 
(see the
+    // ITTest classes), neither of which exists when the functional suite 
runs. Invoke the
+    // entry point that process dispatches to for the ARCHIVE command instead, 
with the
+    // arguments the shell invocation used:
+    // trigger archival --minCommits 2 --maxCommits 3 
--commitsRetainedByCleaner 1 --enableMetadata false
+    // The same 6-commit / 2-3-1 scenario against ArchiveExecutorUtils.archive 
is already covered by
+    // TestArchiveCommitsProcedure in hudi-spark, so what this test adds is 
coverage of the CLI wrapper.
+    assertEquals(0, SparkMain.archive(jsc(), 2, 3, 1, false, tablePath));
+
     metaClient = HoodieTableMetaClient.reload(metaClient);
 
-    //get instants in the active timeline only returns the latest state of the 
commit
-    //therefore we expect 2 instants because minCommits is 2
+    // get instants in the active timeline only returns the latest state of 
the commit
+    // therefore we expect 2 instants because minCommits is 2
     assertEquals(2, metaClient.getActiveTimeline().countInstants());
 
-    //get instants in the archived timeline returns all instants in the commit
-    //therefore we expect 12 instants because 6 commits - 2 commits in active 
timeline = 4 in archived
-    //since each commit is completed, there are 3 instances per commit 
(requested, inflight, completed)
-    //and 3 instances per commit * 4 commits = 12 instances
-    assertEquals(12, metaClient.getArchivedTimeline().countInstants());
+    // 6 commits - 2 kept in the active timeline = 4 archived. The LSM 
archived timeline
+    // of table version 8 and above holds a single entry per instant (the 
legacy log
+    // format archived requested, inflight and completed as separate entries), 
so the
+    // archived timeline counts 4 instants.
+    assertEquals(4, metaClient.getArchivedTimeline().countInstants());
   }
 
 }
-
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestArchivedCommitsCommand.java
 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestArchivedCommitsCommand.java
index 28b1ecd8a8aa..7d476c51dc96 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestArchivedCommitsCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestArchivedCommitsCommand.java
@@ -18,6 +18,11 @@
 
 package org.apache.hudi.cli.commands;
 
+import org.apache.hudi.avro.model.HoodieActionInstant;
+import org.apache.hudi.avro.model.HoodieCleanMetadata;
+import org.apache.hudi.avro.model.HoodieCleanerPlan;
+import org.apache.hudi.avro.model.HoodieRequestedReplaceMetadata;
+import org.apache.hudi.avro.model.HoodieSavepointMetadata;
 import org.apache.hudi.cli.HoodieCLI;
 import org.apache.hudi.cli.HoodiePrintHelper;
 import org.apache.hudi.cli.TableHeader;
@@ -25,17 +30,37 @@ import 
org.apache.hudi.cli.functional.CLIFunctionalTestHarness;
 import org.apache.hudi.cli.testutils.HoodieTestCommitMetadataGenerator;
 import org.apache.hudi.cli.testutils.HoodieTestCommitUtilities;
 import org.apache.hudi.cli.testutils.ShellEvaluationResultUtil;
+import org.apache.hudi.client.timeline.ActiveActionWithDetails;
 import org.apache.hudi.client.timeline.HoodieTimelineArchiver;
+import org.apache.hudi.client.timeline.LSMTimelineWriter;
+import org.apache.hudi.client.timeline.TimelineArchiverV1;
 import org.apache.hudi.client.timeline.TimelineArchiverV2;
+import org.apache.hudi.common.HoodieCleanStat;
+import org.apache.hudi.common.config.HoodieMetadataConfig;
+import org.apache.hudi.common.engine.LocalTaskContextSupplier;
+import org.apache.hudi.common.model.HoodieCleaningPolicy;
 import org.apache.hudi.common.model.HoodieCommitMetadata;
+import org.apache.hudi.common.model.HoodieReplaceCommitMetadata;
+import org.apache.hudi.common.model.HoodieWriteStat;
+import org.apache.hudi.common.model.WriteOperationType;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.HoodieTableVersion;
+import org.apache.hudi.common.table.timeline.HoodieArchivedTimeline;
 import org.apache.hudi.common.table.timeline.HoodieInstant;
 import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import org.apache.hudi.common.table.timeline.MetadataConversionUtils;
+import org.apache.hudi.common.table.timeline.TimelineMetadataUtils;
+import 
org.apache.hudi.common.table.timeline.versioning.clean.CleanPlanV2MigrationHandler;
 import org.apache.hudi.common.table.view.FileSystemViewStorageConfig;
-import org.apache.hudi.common.testutils.HoodieTestUtils;
+import org.apache.hudi.common.testutils.HoodieTestTable;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.collection.Pair;
 import org.apache.hudi.config.HoodieArchivalConfig;
 import org.apache.hudi.config.HoodieCleanConfig;
+import org.apache.hudi.config.HoodieIndexConfig;
 import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.index.HoodieIndex;
+import org.apache.hudi.storage.StoragePath;
 import org.apache.hudi.table.HoodieSparkTable;
 
 import org.junit.jupiter.api.BeforeEach;
@@ -45,12 +70,22 @@ import 
org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.shell.Shell;
 
+import java.io.IOException;
+import java.io.OutputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
 
-import static 
org.apache.hudi.common.testutils.HoodieTestUtils.INSTANT_GENERATOR;
+import static 
org.apache.hudi.common.testutils.HoodieTestDataGenerator.DEFAULT_FIRST_PARTITION_PATH;
+import static 
org.apache.hudi.common.testutils.HoodieTestDataGenerator.DEFAULT_SECOND_PARTITION_PATH;
+import static org.apache.hudi.common.util.CleanerUtils.convertCleanMetadata;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -60,10 +95,22 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 @SpringBootTest(properties = {"spring.shell.interactive.enabled=false", 
"spring.shell.command.script.enabled=false"})
 public class TestArchivedCommitsCommand extends CLIFunctionalTestHarness {
 
+  /**
+   * The clean and the replace commit that the fixture archives along with the 
plain commits.
+   * The clean has to be older than the newest archived write instant for 
TimelineArchiverV2 to
+   * pick it up, and its earliest instant to retain has to be past the replace 
commit so that the
+   * clustering guard of the archiver does not hold the replace commit back.
+   */
+  private static final String CLEAN_INSTANT = "101";
+  private static final String REPLACE_COMMIT_INSTANT = "102";
+  private static final String EARLIEST_COMMIT_TO_RETAIN = "103";
+
   @Autowired
   private Shell shell;
 
   private String tablePath;
+  private HoodieCleanMetadata cleanMetadata;
+  private HoodieReplaceCommitMetadata replaceCommitMetadata;
 
   @BeforeEach
   public void init() throws Exception {
@@ -75,7 +122,7 @@ public class TestArchivedCommitsCommand extends 
CLIFunctionalTestHarness {
 
     new TableCommand().createTable(
         tablePath, tableName,
-        "COPY_ON_WRITE", "", 1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "COPY_ON_WRITE", "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
 
     HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
 
@@ -86,24 +133,20 @@ public class TestArchivedCommitsCommand extends 
CLIFunctionalTestHarness {
         
.withCleanConfig(HoodieCleanConfig.newBuilder().retainCommits(1).build())
         .withFileSystemViewConfig(FileSystemViewStorageConfig.newBuilder()
             .withRemoteServerPort(timelineServicePort).build())
+        
.withMetadataConfig(HoodieMetadataConfig.newBuilder().enable(false).build())
         .forTable("test-trip-table").build();
 
-    // Create six commits
-    for (int i = 100; i < 106; i++) {
+    // Create seven write instants (a commit, a replace commit and five more 
commits) plus one
+    // clean, so that archival keeps the four newest write instants and 
archives 100, 101, 102
+    // and 103
+    HoodieTestCommitMetadataGenerator.createCommitFileWithMetadata(tablePath, 
"100", storageConf());
+    cleanMetadata = addClean(metaClient, CLEAN_INSTANT, 
EARLIEST_COMMIT_TO_RETAIN);
+    replaceCommitMetadata = addReplaceCommit(metaClient, 
REPLACE_COMMIT_INSTANT);
+    for (int i = 103; i < 108; i++) {
       String timestamp = String.valueOf(i);
-      // Requested Compaction
-      
HoodieTestCommitMetadataGenerator.createCompactionAuxiliaryMetadata(tablePath,
-          INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.REQUESTED, 
HoodieTimeline.COMPACTION_ACTION, timestamp), storageConf());
-      // Inflight Compaction
-      
HoodieTestCommitMetadataGenerator.createCompactionAuxiliaryMetadata(tablePath,
-          INSTANT_GENERATOR.createNewInstant(HoodieInstant.State.INFLIGHT, 
HoodieTimeline.COMPACTION_ACTION, timestamp), storageConf());
       
HoodieTestCommitMetadataGenerator.createCommitFileWithMetadata(tablePath, 
timestamp, storageConf());
     }
 
-    // Simulate a compaction commit in metadata table timeline
-    // so the archival in data table can happen
-    HoodieTestUtils.createCompactionCommitInMetadataTable(storageConf(), 
tablePath, "105");
-
     metaClient = HoodieTableMetaClient.reload(metaClient);
     // reload the timeline and get all the commits before archive
     
metaClient.getActiveTimeline().reload().getAllCommitsTimeline().filterCompletedInstants();
@@ -131,31 +174,13 @@ public class TestArchivedCommitsCommand extends 
CLIFunctionalTestHarness {
         
.addTableHeaderField("total_updated_records_compacted").addTableHeaderField("total_write_bytes")
         .addTableHeaderField("total_write_errors");
 
-    // Generate expected data
+    // Generate expected data: the stats only cover the archived commit and 
delta commit
+    // instants, so the archived clean and replace commit contribute no row. 
The archived
+    // timeline holds one entry per instant, with one write stat for each of 
the two
+    // partitions, ordered by partition path
     final List<Comparable[]> rows = new ArrayList<>();
-    for (int i = 100; i < 102; i++) {
-      String instant = String.valueOf(i);
-      for (int j = 0; j < 3; j++) {
-        Comparable[] defaultComp = new Comparable[] {"commit", instant,
-            HoodieTestCommitMetadataGenerator.DEFAULT_SECOND_PARTITION_PATH,
-            HoodieTestCommitMetadataGenerator.DEFAULT_FILEID,
-            HoodieTestCommitMetadataGenerator.DEFAULT_PRE_COMMIT,
-            HoodieTestCommitMetadataGenerator.DEFAULT_NUM_WRITES,
-            HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE,
-            HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE,
-            HoodieTestCommitMetadataGenerator.DEFAULT_NUM_UPDATE_WRITES,
-            HoodieTestCommitMetadataGenerator.DEFAULT_NULL_VALUE,
-            HoodieTestCommitMetadataGenerator.DEFAULT_TOTAL_LOG_BLOCKS,
-            HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE,
-            HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE,
-            HoodieTestCommitMetadataGenerator.DEFAULT_TOTAL_LOG_RECORDS,
-            HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE,
-            HoodieTestCommitMetadataGenerator.DEFAULT_TOTAL_WRITE_BYTES,
-            HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE};
-        rows.add(defaultComp.clone());
-        defaultComp[2] = 
HoodieTestCommitMetadataGenerator.DEFAULT_FIRST_PARTITION_PATH;
-        rows.add(defaultComp);
-      }
+    for (String instant : Arrays.asList("100", "103")) {
+      rows.addAll(writeStatRows("commit", instant));
     }
 
     String expectedResult = HoodiePrintHelper.print(
@@ -174,15 +199,13 @@ public class TestArchivedCommitsCommand extends 
CLIFunctionalTestHarness {
     assertTrue(ShellEvaluationResultUtil.isSuccess(cmdResult));
     final List<Comparable[]> rows = new ArrayList<>();
 
-    // Test default skipMetadata and limit 5
+    // Test default skipMetadata and limit 5. The archived timeline holds one 
entry
+    // per instant, so each instant shows up as a single row
     TableHeader header = new 
TableHeader().addTableHeaderField("CommitTime").addTableHeaderField("CommitType");
-    Comparable[] result1 = new Comparable[] {"100", "commit"};
-    Comparable[] result2 = new Comparable[] {"101", "commit"};
-    rows.add(result1);
-    rows.add(result1);
-    rows.add(result1);
-    rows.add(result2);
-    rows.add(result2);
+    rows.add(new Comparable[] {"100", "commit"});
+    rows.add(new Comparable[] {CLEAN_INSTANT, "clean"});
+    rows.add(new Comparable[] {REPLACE_COMMIT_INSTANT, "replacecommit"});
+    rows.add(new Comparable[] {"103", "commit"});
     String expected = HoodiePrintHelper.print(header, new HashMap<>(), "", 
false, 5, false, rows);
     expected = removeNonWordAndStripSpace(expected);
     String got = removeNonWordAndStripSpace(cmdResult.toString());
@@ -194,20 +217,309 @@ public class TestArchivedCommitsCommand extends 
CLIFunctionalTestHarness {
 
     rows.clear();
 
-    for (int i = 100; i < 102; i++) {
-      String instant = String.valueOf(i);
-      // Since HoodiePrintHelper order data by default, need to order 
commitMetadata
-      HoodieCommitMetadata metadata = 
HoodieTestCommitMetadataGenerator.generateCommitMetadata(tablePath, instant);
-      Comparable[] result = new Comparable[] {
-          instant, "commit", 
HoodieTestCommitUtilities.convertAndOrderCommitMetadata(metadata)};
-      rows.add(result);
-      rows.add(result);
-      rows.add(result);
-    }
+    // Since HoodiePrintHelper order data by default, need to order 
commitMetadata
+    rows.add(new Comparable[] {"100", "commit", commitDetails("100")});
+    rows.add(new Comparable[] {CLEAN_INSTANT, "clean", cleanMetadata});
+    rows.add(new Comparable[] {REPLACE_COMMIT_INSTANT, "replacecommit", 
orderedAvroReplaceCommitMetadata()});
+    rows.add(new Comparable[] {"103", "commit", commitDetails("103")});
     header = header.addTableHeaderField("CommitDetails");
     expected = HoodiePrintHelper.print(header, new HashMap<>(), "", false, 0, 
false, rows);
     expected = removeNonWordAndStripSpace(expected);
     got = removeNonWordAndStripSpace(cmdResult.toString());
     assertEquals(expected, got);
+
+    // Test with Metadata and a limit, the path that loads only the rendered 
instants
+    cmdResult = shell.evaluate(() -> "show archived commits --skipMetadata 
false --limit 2");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(cmdResult));
+
+    rows.clear();
+    rows.add(new Comparable[] {"100", "commit", commitDetails("100")});
+    rows.add(new Comparable[] {CLEAN_INSTANT, "clean", cleanMetadata});
+    expected = HoodiePrintHelper.print(header, new HashMap<>(), "", false, 2, 
false, rows);
+    expected = removeNonWordAndStripSpace(expected);
+    got = removeNonWordAndStripSpace(cmdResult.toString());
+    assertEquals(expected, got);
+  }
+
+  /**
+   * With no sort field the printer only renders the leading rows, so the 
metadata of the
+   * instants past the limit is never loaded. isEmpty(instant) on the archived 
timeline reports
+   * whether that instant's details are held in memory.
+   */
+  @Test
+  public void testShowCommitsLoadsOnlyTheRenderedInstants() {
+    HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
+    HoodieArchivedTimeline archivedTimeline = 
metaClient.getArchivedTimeline("", false);
+    Map<String, HoodieInstant> byTime = archivedTimeline.getInstants().stream()
+        .collect(Collectors.toMap(HoodieInstant::requestedTime, instant -> 
instant));
+
+    List<Comparable[]> rows = 
ArchivedCommitsCommand.readArchivedCommits(archivedTimeline, false, false, "", 
2);
+    assertEquals(Arrays.asList("100", CLEAN_INSTANT),
+        rows.stream().map(row -> row[0]).collect(Collectors.toList()));
+    assertFalse(archivedTimeline.isEmpty(byTime.get("100")));
+    assertFalse(archivedTimeline.isEmpty(byTime.get(CLEAN_INSTANT)));
+    assertTrue(archivedTimeline.isEmpty(byTime.get(REPLACE_COMMIT_INSTANT)));
+    assertTrue(archivedTimeline.isEmpty(byTime.get("103")));
+
+    // a sort field makes every row a candidate, so everything is loaded
+    archivedTimeline = metaClient.getArchivedTimeline("", false);
+    rows = ArchivedCommitsCommand.readArchivedCommits(archivedTimeline, false, 
false, "CommitTime", 2);
+    assertEquals(4, rows.size());
+    assertFalse(archivedTimeline.isEmpty(byTime.get("103")));
+
+    // the stats fill a limit of one row from the two write stats of instant 
100 and stop there
+    archivedTimeline = metaClient.getArchivedTimeline("", false);
+    List<Comparable[]> stats = 
ArchivedCommitsCommand.readCommitStatsFromArchivedTimeline(archivedTimeline, 
"", 1);
+    assertEquals(2, stats.size());
+    assertTrue(archivedTimeline.isEmpty(byTime.get("103")));
+  }
+
+  /**
+   * The upgrade to the LSM timeline copies every entry of a legacy archive 
across, so the archived
+   * timeline can hold actions that TimelineArchiverV2 never writes, such as a 
savepoint. Their
+   * payload renders through the schema it carries instead of failing the 
listing.
+   */
+  @Test
+  public void testShowCommitsRendersActionsWithoutTypedReader() throws 
Exception {
+    HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
+    HoodieSavepointMetadata savepointMetadata = 
TimelineMetadataUtils.convertSavepointMetadata(
+        "user", "savepoint copied across by the upgrade",
+        Collections.singletonMap(DEFAULT_FIRST_PARTITION_PATH, 
Collections.singletonList("file-1")));
+    HoodieInstant savepoint = 
metaClient.getInstantGenerator().createNewInstant(
+        HoodieInstant.State.COMPLETED, HoodieTimeline.SAVEPOINT_ACTION, "099", 
"099");
+    byte[] payload = 
TimelineMetadataUtils.serializeAvroMetadata(savepointMetadata, 
HoodieSavepointMetadata.class).get();
+    HoodieWriteConfig cfg = HoodieWriteConfig.newBuilder().withPath(tablePath)
+        
.withIndexConfig(HoodieIndexConfig.newBuilder().withIndexType(HoodieIndex.IndexType.INMEMORY).build())
+        .withMarkersType("DIRECT").build();
+    LSMTimelineWriter.getInstance(cfg, new LocalTaskContextSupplier(), 
metaClient).write(
+        
Collections.singletonList(ActiveActionWithDetails.fromInstantAndDetails(
+            Collections.singletonList(Pair.of(savepoint, 
Option.of(payload))))),
+        Option.empty(), Option.empty());
+
+    Object cmdResult = shell.evaluate(() -> "show archived commits 
--skipMetadata false --limit 0");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(cmdResult));
+
+    TableHeader header = new 
TableHeader().addTableHeaderField("CommitTime").addTableHeaderField("CommitType")
+        .addTableHeaderField("CommitDetails");
+    final List<Comparable[]> rows = new ArrayList<>();
+    rows.add(new Comparable[] {"099", "savepoint", savepointMetadata});
+    rows.add(new Comparable[] {"100", "commit", commitDetails("100")});
+    rows.add(new Comparable[] {CLEAN_INSTANT, "clean", cleanMetadata});
+    rows.add(new Comparable[] {REPLACE_COMMIT_INSTANT, "replacecommit", 
orderedAvroReplaceCommitMetadata()});
+    rows.add(new Comparable[] {"103", "commit", commitDetails("103")});
+    String expected = removeNonWordAndStripSpace(
+        HoodiePrintHelper.print(header, new HashMap<>(), "", false, 0, false, 
rows));
+    assertEquals(expected, removeNonWordAndStripSpace(cmdResult.toString()));
+  }
+
+  /**
+   * Test for both show archived commands against a table created at version 
6, whose archived
+   * instants live in the legacy log format under the archive folder instead 
of in an LSM
+   * timeline.
+   */
+  @Test
+  public void testShowArchivedCommitsOnLegacyArchive() throws Exception {
+    String legacyTableName = tableName("_legacy_table");
+    String legacyTablePath = tablePath(legacyTableName);
+
+    // createTable also connects the CLI to the new table
+    new TableCommand().createTable(
+        legacyTablePath, legacyTableName, "COPY_ON_WRITE", "",
+        HoodieTableVersion.SIX.versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
+
+    HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
+
+    HoodieWriteConfig cfg = 
HoodieWriteConfig.newBuilder().withPath(legacyTablePath)
+        
.withSchema(HoodieTestCommitMetadataGenerator.TRIP_EXAMPLE_SCHEMA).withParallelism(2,
 2)
+        .withWriteTableVersion(HoodieTableVersion.SIX.versionCode())
+        .withAutoUpgradeVersion(false)
+        
.withArchivalConfig(HoodieArchivalConfig.newBuilder().archiveCommitsWith(4, 
5).build())
+        
.withCleanConfig(HoodieCleanConfig.newBuilder().retainCommits(1).build())
+        .withFileSystemViewConfig(FileSystemViewStorageConfig.newBuilder()
+            .withRemoteServerPort(timelineServicePort).build())
+        
.withMetadataConfig(HoodieMetadataConfig.newBuilder().enable(false).build())
+        .forTable(legacyTableName).build();
+
+    // Six commits, of which archival keeps the four newest and archives 100 
and 101
+    List<HoodieCommitMetadata> commitMetadata = new ArrayList<>();
+    for (int i = 100; i < 106; i++) {
+      commitMetadata.add(createLegacyCommit(metaClient, legacyTablePath, 
String.valueOf(i)));
+    }
+
+    metaClient = HoodieTableMetaClient.reload(metaClient);
+    HoodieSparkTable table = HoodieSparkTable.create(cfg, context(), 
metaClient);
+    HoodieTimelineArchiver archiver = new TimelineArchiverV1(cfg, table);
+    archiver.archiveIfRequired(context());
+
+    TableHeader statsHeader = new 
TableHeader().addTableHeaderField("action").addTableHeaderField("instant")
+        
.addTableHeaderField("partition").addTableHeaderField("file_id").addTableHeaderField("prev_instant")
+        
.addTableHeaderField("num_writes").addTableHeaderField("num_inserts").addTableHeaderField("num_deletes")
+        
.addTableHeaderField("num_update_writes").addTableHeaderField("total_log_files")
+        
.addTableHeaderField("total_log_blocks").addTableHeaderField("total_corrupt_log_blocks")
+        
.addTableHeaderField("total_rollback_blocks").addTableHeaderField("total_log_records")
+        
.addTableHeaderField("total_updated_records_compacted").addTableHeaderField("total_write_bytes")
+        .addTableHeaderField("total_write_errors");
+
+    // The pre table version 8 archive keeps one entry per instant state. The 
requested entry
+    // of a commit carries no metadata and the inflight one only the 
in-progress stats, so the
+    // legacy stats reader renders the completed entry of each archived 
instant, with its stat
+    // rows ordered by partition path like the archived timeline path
+    final List<Comparable[]> statsRows = new ArrayList<>();
+    for (String instant : Arrays.asList("100", "101")) {
+      statsRows.addAll(writeStatRows("commit", instant));
+    }
+    String expectedStats = removeNonWordAndStripSpace(
+        HoodiePrintHelper.print(statsHeader, new HashMap<>(), "", false, 0, 
false, statsRows));
+
+    Object result = shell.evaluate(() -> "show archived commit stats");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+    assertEquals(expectedStats, removeNonWordAndStripSpace(result.toString()));
+
+    // the explicit folder pattern reads the very same archive files
+    result = shell.evaluate(() -> "show archived commit stats 
--archiveFolderPattern archived/.commits_.archive*");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+    assertEquals(expectedStats, removeNonWordAndStripSpace(result.toString()));
+
+    // show archived commits reads the same archive through 
ArchivedTimelineV1, which keeps only
+    // the completed entry of each instant and caches its metadata as the 
archived JSON rendering
+    TableHeader header = new 
TableHeader().addTableHeaderField("CommitTime").addTableHeaderField("CommitType")
+        .addTableHeaderField("CommitDetails");
+    final List<Comparable[]> rows = new ArrayList<>();
+    rows.add(new Comparable[] {"100", "commit",
+        
HoodieTestCommitUtilities.convertAndOrderCommitMetadata(commitMetadata.get(0))});
+    rows.add(new Comparable[] {"101", "commit",
+        
HoodieTestCommitUtilities.convertAndOrderCommitMetadata(commitMetadata.get(1))});
+    String expected = removeNonWordAndStripSpace(
+        HoodiePrintHelper.print(header, new HashMap<>(), "", false, 0, false, 
rows));
+
+    result = shell.evaluate(() -> "show archived commits --skipMetadata 
false");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+    assertEquals(expected, removeNonWordAndStripSpace(result.toString()));
+  }
+
+  /**
+   * The metadata of one of the plain commits of the fixture, rendered the way 
the command
+   * renders it, with the partition keyed map ordered by partition path.
+   */
+  private Comparable commitDetails(String instantTime) throws Exception {
+    return HoodieTestCommitUtilities.convertAndOrderCommitMetadata(
+        HoodieTestCommitMetadataGenerator.generateCommitMetadata(tablePath, 
instantTime));
+  }
+
+  /**
+   * The archived replace commit rendered the way the command renders it, with 
both partition
+   * keyed maps ordered by partition path.
+   */
+  private org.apache.hudi.avro.model.HoodieReplaceCommitMetadata 
orderedAvroReplaceCommitMetadata() {
+    org.apache.hudi.avro.model.HoodieReplaceCommitMetadata avroMetadata =
+        
MetadataConversionUtils.convertCommitMetadataToAvro(replaceCommitMetadata);
+    avroMetadata.setPartitionToWriteStats(new 
TreeMap<>(avroMetadata.getPartitionToWriteStats()));
+    avroMetadata.setPartitionToReplaceFileIds(new 
TreeMap<>(avroMetadata.getPartitionToReplaceFileIds()));
+    return avroMetadata;
+  }
+
+  /**
+   * The stat rows of a commit written by {@link 
HoodieTestCommitMetadataGenerator}, ordered by
+   * partition path.
+   */
+  private static List<Comparable[]> writeStatRows(String action, String 
instant) {
+    return Arrays.asList(
+        writeStatRow(action, instant, DEFAULT_SECOND_PARTITION_PATH),
+        writeStatRow(action, instant, DEFAULT_FIRST_PARTITION_PATH));
+  }
+
+  private static Comparable[] writeStatRow(String action, String instant, 
String partitionPath) {
+    return new Comparable[] {action, instant, partitionPath,
+        HoodieTestCommitMetadataGenerator.DEFAULT_FILEID,
+        HoodieTestCommitMetadataGenerator.DEFAULT_PRE_COMMIT,
+        HoodieTestCommitMetadataGenerator.DEFAULT_NUM_WRITES,
+        HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE,
+        HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE,
+        HoodieTestCommitMetadataGenerator.DEFAULT_NUM_UPDATE_WRITES,
+        HoodieTestCommitMetadataGenerator.DEFAULT_NULL_VALUE,
+        HoodieTestCommitMetadataGenerator.DEFAULT_TOTAL_LOG_BLOCKS,
+        HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE,
+        HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE,
+        HoodieTestCommitMetadataGenerator.DEFAULT_TOTAL_LOG_RECORDS,
+        HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE,
+        HoodieTestCommitMetadataGenerator.DEFAULT_TOTAL_WRITE_BYTES,
+        HoodieTestCommitMetadataGenerator.DEFAULT_OTHER_VALUE};
+  }
+
+  /**
+   * Writes a completed clean whose plan retains from {@code 
earliestCommitToRetain}.
+   */
+  private static HoodieCleanMetadata addClean(HoodieTableMetaClient 
metaClient, String instantTime,
+                                              String earliestCommitToRetain) 
throws IOException {
+    HoodieCleanerPlan cleanerPlan = new HoodieCleanerPlan(
+        new HoodieActionInstant(earliestCommitToRetain, 
HoodieTimeline.COMMIT_ACTION, HoodieInstant.State.COMPLETED.name()),
+        "", HoodieCleaningPolicy.KEEP_LATEST_COMMITS.name(), new HashMap<>(),
+        CleanPlanV2MigrationHandler.VERSION, new HashMap<>(), new 
ArrayList<>(), Collections.emptyMap());
+    HoodieCleanStat cleanStat = HoodieCleanStat.builder()
+        .withPolicy(HoodieCleaningPolicy.KEEP_LATEST_COMMITS)
+        .withPartitionPath(DEFAULT_FIRST_PARTITION_PATH)
+        .withEarliestCommitToRetain(earliestCommitToRetain)
+        .withLastCompletedCommitTimestamp("")
+        .build();
+    HoodieCleanMetadata metadata = convertCleanMetadata(
+        instantTime, Option.of(0L), Collections.singletonList(cleanStat), 
Collections.emptyMap());
+    HoodieTestTable.of(metaClient).addClean(instantTime, cleanerPlan, 
metadata);
+    return metadata;
+  }
+
+  /**
+   * Writes a completed insert overwrite replace commit over the two default 
partitions.
+   */
+  private static HoodieReplaceCommitMetadata 
addReplaceCommit(HoodieTableMetaClient metaClient,
+                                                              String 
instantTime) throws Exception {
+    HoodieReplaceCommitMetadata metadata = new HoodieReplaceCommitMetadata();
+    metadata.setOperationType(WriteOperationType.INSERT_OVERWRITE);
+    for (String partitionPath : Arrays.asList(DEFAULT_FIRST_PARTITION_PATH, 
DEFAULT_SECOND_PARTITION_PATH)) {
+      HoodieWriteStat writeStat = new HoodieWriteStat();
+      writeStat.setPartitionPath(partitionPath);
+      writeStat.setPath(HoodieTestCommitMetadataGenerator.DEFAULT_PATH);
+      writeStat.setFileId(HoodieTestCommitMetadataGenerator.DEFAULT_FILEID);
+      
writeStat.setTotalWriteBytes(HoodieTestCommitMetadataGenerator.DEFAULT_TOTAL_WRITE_BYTES);
+      
writeStat.setPrevCommit(HoodieTestCommitMetadataGenerator.DEFAULT_PRE_COMMIT);
+      
writeStat.setNumWrites(HoodieTestCommitMetadataGenerator.DEFAULT_NUM_WRITES);
+      
writeStat.setNumUpdateWrites(HoodieTestCommitMetadataGenerator.DEFAULT_NUM_UPDATE_WRITES);
+      
writeStat.setTotalLogBlocks(HoodieTestCommitMetadataGenerator.DEFAULT_TOTAL_LOG_BLOCKS);
+      
writeStat.setTotalLogRecords(HoodieTestCommitMetadataGenerator.DEFAULT_TOTAL_LOG_RECORDS);
+      metadata.addWriteStat(partitionPath, writeStat);
+      metadata.addReplaceFileId(partitionPath, 
HoodieTestCommitMetadataGenerator.DEFAULT_FILEID);
+    }
+    HoodieRequestedReplaceMetadata requestedReplaceMetadata = 
HoodieRequestedReplaceMetadata.newBuilder()
+        .setOperationType(WriteOperationType.INSERT_OVERWRITE.name())
+        .setVersion(1)
+        .setExtraMetadata(Collections.emptyMap())
+        .build();
+    HoodieTestTable.of(metaClient)
+        .addReplaceCommit(instantTime, Option.of(requestedReplaceMetadata), 
Option.empty(), metadata);
+    return metadata;
+  }
+
+  /**
+   * Writes the requested, the inflight and the completed file of one commit 
through the meta
+   * client of the table, so that the files follow the layout of the table 
version and look like
+   * what a writer leaves behind: an empty requested file, and the commit 
metadata in the inflight
+   * and the completed file.
+   */
+  private static HoodieCommitMetadata createLegacyCommit(HoodieTableMetaClient 
metaClient, String basePath,
+                                                         String instantTime) 
throws Exception {
+    HoodieCommitMetadata metadata = 
HoodieTestCommitMetadataGenerator.generateCommitMetadata(basePath, instantTime);
+    // the archiver drops the commit metadata of an entry whose operation type 
is UNKNOWN
+    metadata.setOperationType(WriteOperationType.INSERT);
+    metaClient.getStorage().create(new 
StoragePath(metaClient.getTimelinePath(),
+        
metaClient.getInstantFileNameGenerator().makeRequestedCommitFileName(instantTime)),
 true).close();
+    List<String> fileNames = Arrays.asList(
+        
metaClient.getInstantFileNameGenerator().makeInflightCommitFileName(instantTime),
+        
metaClient.getInstantFileNameGenerator().makeCommitFileName(instantTime));
+    for (String fileName : fileNames) {
+      StoragePath path = new StoragePath(metaClient.getTimelinePath(), 
fileName);
+      try (OutputStream os = metaClient.getStorage().create(path, true)) {
+        
metaClient.getCommitMetadataSerDe().getInstantWriter(metadata).get().writeToStream(os);
+      }
+    }
+    return metadata;
   }
 }
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCleansCommand.java 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCleansCommand.java
index c1972c6e488a..18f364ca90d0 100644
--- a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCleansCommand.java
+++ b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCleansCommand.java
@@ -30,10 +30,10 @@ import org.apache.hudi.common.model.HoodieCleaningPolicy;
 import org.apache.hudi.common.model.HoodieCommitMetadata;
 import org.apache.hudi.common.model.HoodieTableType;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.table.timeline.HoodieActiveTimeline;
 import org.apache.hudi.common.table.timeline.HoodieInstant;
 import org.apache.hudi.common.table.timeline.HoodieTimeline;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
 import org.apache.hudi.common.testutils.HoodieTestDataGenerator;
 import org.apache.hudi.common.testutils.HoodieTestTable;
 import org.apache.hudi.common.util.HoodieStorageUtils;
@@ -86,7 +86,7 @@ public class TestCleansCommand extends 
CLIFunctionalTestHarness {
     // Create table and connect
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.COPY_ON_WRITE.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
 
     StorageConfiguration<?> conf = HoodieCLI.conf;
 
@@ -177,12 +177,10 @@ public class TestCleansCommand extends 
CLIFunctionalTestHarness {
         
.addTableHeaderField(HoodieTableHeaderFields.HEADER_TOTAL_FILES_SUCCESSFULLY_DELETED)
         
.addTableHeaderField(HoodieTableHeaderFields.HEADER_TOTAL_FAILED_DELETIONS);
 
-    // There should be two partition path
+    // There should be two partition paths, rendered in partition order
     List<Comparable[]> rows = new ArrayList<>();
     rows.add(new Comparable[] 
{HoodieTestCommitMetadataGenerator.DEFAULT_SECOND_PARTITION_PATH,
         HoodieCleaningPolicy.KEEP_LATEST_COMMITS, "1", "0"});
-    rows.add(new Comparable[] 
{HoodieTestCommitMetadataGenerator.DEFAULT_THIRD_PARTITION_PATH,
-        HoodieCleaningPolicy.KEEP_LATEST_COMMITS, "0", "0"});
     rows.add(new Comparable[] 
{HoodieTestCommitMetadataGenerator.DEFAULT_FIRST_PARTITION_PATH,
         HoodieCleaningPolicy.KEEP_LATEST_COMMITS, "1", "0"});
 
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCommitsCommand.java 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCommitsCommand.java
index 54f2de8bc835..2166448ffd10 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCommitsCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCommitsCommand.java
@@ -32,12 +32,11 @@ import org.apache.hudi.common.config.HoodieMetadataConfig;
 import org.apache.hudi.common.fs.FSUtils;
 import org.apache.hudi.common.model.HoodieTableType;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.table.timeline.HoodieInstant;
 import org.apache.hudi.common.table.timeline.HoodieInstantTimeGenerator;
 import org.apache.hudi.common.table.timeline.HoodieTimeline;
 import org.apache.hudi.common.table.timeline.InstantFileNameGenerator;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
-import 
org.apache.hudi.common.table.timeline.versioning.v1.InstantFileNameGeneratorV1;
 import org.apache.hudi.common.table.view.FileSystemViewStorageConfig;
 import org.apache.hudi.common.testutils.HoodieTestDataGenerator;
 import org.apache.hudi.common.testutils.HoodieTestUtils;
@@ -105,7 +104,7 @@ public class TestCommitsCommand extends 
CLIFunctionalTestHarness {
     // Create table and connect
     new TableCommand().createTable(
         tablePath1, tableName1, HoodieTableType.COPY_ON_WRITE.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
     metaClient = HoodieCLI.getTableMetaClient();
   }
 
@@ -590,7 +589,7 @@ public class TestCommitsCommand extends 
CLIFunctionalTestHarness {
     metaClient = HoodieTableMetaClient.reload(HoodieCLI.getTableMetaClient());
 
     // Create inflight commits
-    InstantFileNameGenerator v1InstantNameGenerator = new 
InstantFileNameGeneratorV1();
+    InstantFileNameGenerator v1InstantNameGenerator = 
metaClient.getInstantFileNameGenerator();
     List<String> fileNames = Arrays.asList(
         v1InstantNameGenerator.makeInflightCommitFileName(oldInstantTime1),
         v1InstantNameGenerator.makeCommitFileName(
@@ -598,7 +597,7 @@ public class TestCommitsCommand extends 
CLIFunctionalTestHarness {
         v1InstantNameGenerator.makeInflightCommitFileName(oldInstantTime3));
     fileNames.forEach(name -> {
       try {
-        Path filePath = new Path(basePath() + "/" + 
HoodieTableMetaClient.METAFOLDER_NAME + "/" + name);
+        Path filePath = new Path(metaClient.getTimelinePath() + "/" + name);
         HoodieTestDataGenerator.createEmptyFile(basePath(), filePath, 
storageConf());
       } catch (IOException ignored) {
         // Exception ignored.
@@ -618,11 +617,13 @@ public class TestCommitsCommand extends 
CLIFunctionalTestHarness {
     assertTrue(output.contains(oldInstantTime3));
 
     // Only one instants should be shown when duration is 15 since 2nd commit 
is a completed commit.
+    // The 3rd instant started 5 mins ago, so it is not older than the 15 min 
lookback window either.
     Object lookupBackIn15MinsResult = shell.evaluate(() -> "commits 
show_inflights --lookbackInMins 15");
     assertTrue(ShellEvaluationResultUtil.isSuccess(lookupBackIn15MinsResult));
     output = lookupBackIn15MinsResult.toString();
     assertTrue(output.contains(oldInstantTime1));
     assertFalse(output.contains(oldInstantTime2));
+    assertFalse(output.contains(oldInstantTime3));
 
     // Only one instant should be shown when duration is 50
     Object lookupBackIn50MinsResult = shell.evaluate(() -> "commits 
show_inflights --lookbackInMins 50");
@@ -630,12 +631,12 @@ public class TestCommitsCommand extends 
CLIFunctionalTestHarness {
     output = lookupBackIn50MinsResult.toString();
     assertTrue(output.contains(oldInstantTime1));
     assertFalse(output.contains(oldInstantTime2));
+    assertFalse(output.contains(oldInstantTime3));
 
     // No instants should be shown when duration is > 60
     Object lookupBackIn70MinsResult = shell.evaluate(() -> "commits 
show_inflights --lookbackInMins 70");
     assertTrue(ShellEvaluationResultUtil.isSuccess(lookupBackIn70MinsResult));
     output = lookupBackIn70MinsResult.toString();
-    assertTrue(output.contains(oldInstantTime1));
-    assertFalse(output.contains(oldInstantTime2));
+    assertEquals("No inflight instants are found.", output);
   }
 }
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCompactionCommand.java
 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCompactionCommand.java
index 001b5181e022..d25abe1789ad 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCompactionCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestCompactionCommand.java
@@ -30,9 +30,9 @@ import org.apache.hudi.common.model.HoodieAvroPayload;
 import org.apache.hudi.common.model.HoodieCommitMetadata;
 import org.apache.hudi.common.model.HoodieTableType;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.table.timeline.HoodieActiveTimeline;
 import org.apache.hudi.common.table.timeline.HoodieInstant;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
 import org.apache.hudi.common.table.view.FileSystemViewStorageConfig;
 import org.apache.hudi.common.testutils.CompactionTestUtils;
 import org.apache.hudi.common.testutils.HoodieTestUtils;
@@ -89,7 +89,7 @@ public class TestCompactionCommand extends 
CLIFunctionalTestHarness {
     // create COW table.
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.COPY_ON_WRITE.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
HoodieAvroPayload.class.getName());
+        "", HoodieTableVersion.current().versionCode(), 
HoodieAvroPayload.class.getName());
 
     // expect HoodieException for COPY_ON_WRITE table.
     assertThrows(HoodieException.class,
@@ -104,7 +104,7 @@ public class TestCompactionCommand extends 
CLIFunctionalTestHarness {
     // create MOR table.
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.MERGE_ON_READ.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
HoodieAvroPayload.class.getName());
+        "", HoodieTableVersion.current().versionCode(), 
HoodieAvroPayload.class.getName());
 
     
CompactionTestUtils.setupAndValidateCompactionOperations(HoodieCLI.getTableMetaClient(),
 false, 3, 4, 3, 3);
 
@@ -136,7 +136,7 @@ public class TestCompactionCommand extends 
CLIFunctionalTestHarness {
     // create MOR table.
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.MERGE_ON_READ.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
HoodieAvroPayload.class.getName());
+        "", HoodieTableVersion.current().versionCode(), 
HoodieAvroPayload.class.getName());
 
     
CompactionTestUtils.setupAndValidateCompactionOperations(HoodieCLI.getTableMetaClient(),
 false, 3, 4, 3, 3);
 
@@ -150,7 +150,7 @@ public class TestCompactionCommand extends 
CLIFunctionalTestHarness {
     // create MOR table.
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.MERGE_ON_READ.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
HoodieAvroPayload.class.getName());
+        "", HoodieTableVersion.current().versionCode(), 
HoodieAvroPayload.class.getName());
 
     
CompactionTestUtils.setupAndValidateCompactionOperations(HoodieCLI.getTableMetaClient(),
 true, 1, 2, 3, 4);
 
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestDiffCommand.java 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestDiffCommand.java
index d4b240b67c57..c80a8f8d954b 100644
--- a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestDiffCommand.java
+++ b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestDiffCommand.java
@@ -30,9 +30,9 @@ import org.apache.hudi.common.model.HoodieAvroPayload;
 import org.apache.hudi.common.model.HoodieCommitMetadata;
 import org.apache.hudi.common.model.HoodieTableType;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.table.timeline.HoodieInstant;
 import org.apache.hudi.common.table.timeline.HoodieTimeline;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
 import org.apache.hudi.common.testutils.HoodieTestDataGenerator;
 import org.apache.hudi.common.testutils.HoodieTestTable;
 import org.apache.hudi.common.util.HoodieStorageUtils;
@@ -86,7 +86,7 @@ public class TestDiffCommand extends CLIFunctionalTestHarness 
{
     // create COW table.
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.COPY_ON_WRITE.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
HoodieAvroPayload.class.getName());
+        "", HoodieTableVersion.current().versionCode(), 
HoodieAvroPayload.class.getName());
 
     StorageConfiguration<?> conf = HoodieCLI.conf;
 
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestFileSystemViewCommand.java
 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestFileSystemViewCommand.java
index 1eaa08959ac3..a495d37fffaa 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestFileSystemViewCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestFileSystemViewCommand.java
@@ -30,8 +30,10 @@ import org.apache.hudi.common.model.FileSlice;
 import org.apache.hudi.common.model.HoodieFileGroup;
 import org.apache.hudi.common.model.HoodieLogFile;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.table.view.HoodieTableFileSystemView;
 import org.apache.hudi.common.table.view.SyncableFileSystemView;
+import org.apache.hudi.common.testutils.FileCreateUtils;
 import org.apache.hudi.common.util.NumericUtils;
 
 import org.junit.jupiter.api.BeforeEach;
@@ -86,7 +88,7 @@ public class TestFileSystemViewCommand extends 
CLIFunctionalTestHarness {
     nonpartitionedTablePath = tablePath(nonpartitionedTableName);
     new TableCommand().createTable(
         nonpartitionedTablePath, nonpartitionedTableName,
-        "COPY_ON_WRITE", "", 1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "COPY_ON_WRITE", "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
 
     HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
 
@@ -110,8 +112,8 @@ public class TestFileSystemViewCommand extends 
CLIFunctionalTestHarness {
         .makeInlineLogFileName(fileId1, HoodieLogFile.DELTA_EXTENSION, 
commitTime2, 0, testWriteToken)));
 
     // Write commit files
-    Files.createFile(Paths.get(nonpartitionedTablePath, ".hoodie", commitTime1 
+ ".commit"));
-    Files.createFile(Paths.get(nonpartitionedTablePath, ".hoodie", commitTime2 
+ ".commit"));
+    FileCreateUtils.createCommit(metaClient, commitTime1);
+    FileCreateUtils.createCommit(metaClient, commitTime2);
 
     // Reload meta client and create fsView
     metaClient = HoodieTableMetaClient.reload(metaClient);
@@ -127,7 +129,7 @@ public class TestFileSystemViewCommand extends 
CLIFunctionalTestHarness {
     partitionedTablePath = tablePath(partitionedTableName);
     new TableCommand().createTable(
         partitionedTablePath, partitionedTableName,
-        "COPY_ON_WRITE", "", 1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "COPY_ON_WRITE", "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
 
     HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
 
@@ -153,8 +155,8 @@ public class TestFileSystemViewCommand extends 
CLIFunctionalTestHarness {
         .makeInlineLogFileName(fileId1, HoodieLogFile.DELTA_EXTENSION, 
commitTime2, 0, testWriteToken)));
 
     // Write commit files
-    Files.createFile(Paths.get(partitionedTablePath, ".hoodie", commitTime1 + 
".commit"));
-    Files.createFile(Paths.get(partitionedTablePath, ".hoodie", commitTime2 + 
".commit"));
+    FileCreateUtils.createCommit(metaClient, commitTime1);
+    FileCreateUtils.createCommit(metaClient, commitTime2);
 
     // Reload meta client and create fsView
     metaClient = HoodieTableMetaClient.reload(metaClient);
@@ -294,12 +296,7 @@ public class TestFileSystemViewCommand extends 
CLIFunctionalTestHarness {
     return rows;
   }
 
-  /**
-   * (
-   * Test case for command 'show fsview latest'.
-   */
-  @Test
-  public void testShowLatestFileSlices() throws IOException {
+  private static Map<String, Function<Object, String>> 
latestFileSlicesConverterMap() {
     Function<Object, String> converterFunction =
         entry -> 
NumericUtils.humanReadableByteCount((Double.parseDouble(entry.toString())));
     Map<String, Function<Object, String>> fieldNameToConverterMap = new 
HashMap<>();
@@ -307,8 +304,11 @@ public class TestFileSystemViewCommand extends 
CLIFunctionalTestHarness {
     
fieldNameToConverterMap.put(HoodieTableHeaderFields.HEADER_TOTAL_DELTA_SIZE, 
converterFunction);
     
fieldNameToConverterMap.put(HoodieTableHeaderFields.HEADER_DELTA_SIZE_SCHEDULED,
 converterFunction);
     
fieldNameToConverterMap.put(HoodieTableHeaderFields.HEADER_DELTA_SIZE_UNSCHEDULED,
 converterFunction);
+    return fieldNameToConverterMap;
+  }
 
-    TableHeader header = new 
TableHeader().addTableHeaderField(HoodieTableHeaderFields.HEADER_PARTITION)
+  private static TableHeader latestFileSlicesHeader() {
+    return new 
TableHeader().addTableHeaderField(HoodieTableHeaderFields.HEADER_PARTITION)
         .addTableHeaderField(HoodieTableHeaderFields.HEADER_FILE_ID)
         .addTableHeaderField(HoodieTableHeaderFields.HEADER_BASE_INSTANT)
         .addTableHeaderField(HoodieTableHeaderFields.HEADER_DATA_FILE)
@@ -321,6 +321,16 @@ public class TestFileSystemViewCommand extends 
CLIFunctionalTestHarness {
         
.addTableHeaderField(HoodieTableHeaderFields.HEADER_DELTA_BASE_UNSCHEDULED)
         
.addTableHeaderField(HoodieTableHeaderFields.HEADER_DELTA_FILES_SCHEDULED)
         
.addTableHeaderField(HoodieTableHeaderFields.HEADER_DELTA_FILES_UNSCHEDULED);
+  }
+
+  /**
+   * (
+   * Test case for command 'show fsview latest'.
+   */
+  @Test
+  public void testShowLatestFileSlices() throws IOException {
+    Map<String, Function<Object, String>> fieldNameToConverterMap = 
latestFileSlicesConverterMap();
+    TableHeader header = latestFileSlicesHeader();
 
     // Test show with partition path '2016/03/15'
     new TableCommand().connect(partitionedTablePath, false, 0, 0, 0,
@@ -351,4 +361,30 @@ public class TestFileSystemViewCommand extends 
CLIFunctionalTestHarness {
     String nonpartitionedResults = 
removeNonWordAndStripSpace(nonpartitionedTable.toString());
     assertEquals(nonpartitionedExpected, nonpartitionedResults);
   }
+
+  /**
+   * Test case for command 'show fsview latest' on a table whose timeline 
holds no completed
+   * instant. The merge branch has nothing to pick a max instant from, so it 
has to fall back to
+   * the unmerged latest file slices instead of dereferencing an empty Option.
+   */
+  @Test
+  public void testShowLatestFileSlicesWithEmptyTimeline() throws IOException {
+    HoodieCLI.conf = storageConf();
+
+    // Create table and connect. No commit is ever written to it.
+    String emptyTableName = "empty_" + tableName();
+    String emptyTablePath = tablePath(emptyTableName);
+    new TableCommand().createTable(
+        emptyTablePath, emptyTableName,
+        "COPY_ON_WRITE", "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
+
+    // --merge defaults to true, so this takes the branch that resolves a max 
instant
+    Object result = shell.evaluate(() -> "show fsview latest");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+
+    // header only, no file slice rows
+    String expected = HoodiePrintHelper.print(latestFileSlicesHeader(), 
latestFileSlicesConverterMap(),
+        "", false, -1, false, new ArrayList<>());
+    assertEquals(removeNonWordAndStripSpace(expected), 
removeNonWordAndStripSpace(result.toString()));
+  }
 }
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestHoodieLogFileCommand.java
 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestHoodieLogFileCommand.java
index cb0c4c742b78..ac7ca0998d1a 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestHoodieLogFileCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestHoodieLogFileCommand.java
@@ -35,13 +35,13 @@ import org.apache.hudi.common.model.HoodieRecord;
 import org.apache.hudi.common.model.HoodieTableType;
 import org.apache.hudi.common.schema.HoodieSchema;
 import org.apache.hudi.common.schema.HoodieSchemaUtils;
+import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.table.log.HoodieLogFormat;
 import org.apache.hudi.common.table.log.HoodieLogFormatWriter;
 import org.apache.hudi.common.table.log.HoodieMergedLogRecordScanner;
 import org.apache.hudi.common.table.log.block.HoodieAvroDataBlock;
 import org.apache.hudi.common.table.log.block.HoodieCommandBlock;
 import org.apache.hudi.common.table.log.block.HoodieLogBlock;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
 import org.apache.hudi.common.testutils.SchemaTestUtil;
 import org.apache.hudi.common.util.HoodieStorageUtils;
 import org.apache.hudi.common.util.Option;
@@ -105,7 +105,7 @@ public class TestHoodieLogFileCommand extends 
CLIFunctionalTestHarness {
     partitionPath = Paths.get(tablePath, 
DEFAULT_FIRST_PARTITION_PATH).toString();
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.MERGE_ON_READ.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
 
     Files.createDirectories(Paths.get(partitionPath));
     storage = HoodieStorageUtils.getStorage(tablePath, storageConf());
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRepairsCommand.java 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRepairsCommand.java
index ea8c4b8817ed..b49325159d62 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRepairsCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRepairsCommand.java
@@ -18,6 +18,9 @@
 
 package org.apache.hudi.cli.commands;
 
+import org.apache.hudi.avro.model.HoodieActionInstant;
+import org.apache.hudi.avro.model.HoodieCleanFileInfo;
+import org.apache.hudi.avro.model.HoodieCleanerPlan;
 import org.apache.hudi.cli.HoodieCLI;
 import org.apache.hudi.cli.HoodiePrintHelper;
 import org.apache.hudi.cli.HoodieTableHeaderFields;
@@ -29,22 +32,33 @@ import org.apache.hudi.client.WriteClientTestUtils;
 import org.apache.hudi.client.WriteStatus;
 import org.apache.hudi.common.fs.FSUtils;
 import org.apache.hudi.common.model.HoodieAvroIndexedRecord;
+import org.apache.hudi.common.model.HoodieCleaningPolicy;
 import org.apache.hudi.common.model.HoodieKey;
 import org.apache.hudi.common.model.HoodieRecord;
 import org.apache.hudi.common.model.HoodieTableType;
 import org.apache.hudi.common.table.HoodieTableConfig;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
+import org.apache.hudi.common.table.HoodieTableVersion;
+import org.apache.hudi.common.table.timeline.HoodieInstant;
+import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import 
org.apache.hudi.common.table.timeline.versioning.clean.CleanPlanV2MigrationHandler;
+import org.apache.hudi.common.testutils.FileCreateUtils;
 import org.apache.hudi.common.testutils.HoodieTestDataGenerator;
 import org.apache.hudi.common.util.HoodieStorageUtils;
 import org.apache.hudi.common.util.PartitionPathEncodeUtils;
 import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.exception.HoodieIOException;
 import org.apache.hudi.hadoop.fs.HadoopFSUtils;
+import org.apache.hudi.io.util.FileIOUtils;
 import org.apache.hudi.keygen.SimpleKeyGenerator;
 import org.apache.hudi.storage.StorageConfiguration;
+import org.apache.hudi.storage.StoragePath;
+import org.apache.hudi.storage.hadoop.HoodieHadoopStorage;
 import org.apache.hudi.testutils.Assertions;
 
+import org.apache.avro.file.DataFileWriter;
 import org.apache.avro.generic.GenericRecord;
+import org.apache.avro.specific.SpecificDatumWriter;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
 import org.apache.logging.log4j.Level;
@@ -63,31 +77,33 @@ import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.shell.Shell;
 
 import java.io.FileInputStream;
+import java.io.FilterInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketTimeoutException;
 import java.net.URL;
-import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
+import java.util.TreeSet;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
 import static 
org.apache.hudi.common.table.HoodieTableConfig.DROP_PARTITION_COLUMNS;
-import static org.apache.hudi.common.table.HoodieTableConfig.NAME;
 import static org.apache.hudi.common.table.HoodieTableConfig.TABLE_CHECKSUM;
-import static 
org.apache.hudi.common.table.HoodieTableConfig.TIMELINE_HISTORY_PATH;
-import static 
org.apache.hudi.common.table.HoodieTableConfig.TIMELINE_LAYOUT_VERSION;
-import static org.apache.hudi.common.table.HoodieTableConfig.TYPE;
-import static org.apache.hudi.common.table.HoodieTableConfig.VERSION;
 import static org.apache.hudi.common.table.HoodieTableConfig.generateChecksum;
 import static org.apache.hudi.common.table.HoodieTableConfig.validateChecksum;
 import static 
org.apache.hudi.common.testutils.HoodieTestDataGenerator.DEFAULT_FIRST_PARTITION_PATH;
 import static 
org.apache.hudi.common.testutils.HoodieTestDataGenerator.TRIP_EXAMPLE_SCHEMA;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -112,7 +128,7 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
     // Create table and connect
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.COPY_ON_WRITE.name(),
-        HoodieTableConfig.TIMELINE_HISTORY_PATH.defaultValue(), 
TimelineLayoutVersion.VERSION_1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        HoodieTableConfig.TIMELINE_HISTORY_PATH.defaultValue(), 
HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
   }
 
   @AfterEach
@@ -126,7 +142,7 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
   @Test
   public void testAddPartitionMetaWithDryRun() throws IOException {
     // create commit instant
-    Files.createFile(Paths.get(tablePath, ".hoodie/timeline/", "100.commit"));
+    FileCreateUtils.createCommit(HoodieCLI.getTableMetaClient(), "100");
 
     // create partition path
     String partition1 = Paths.get(tablePath, 
HoodieTestDataGenerator.DEFAULT_FIRST_PARTITION_PATH).toString();
@@ -161,7 +177,7 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
   @Test
   public void testAddPartitionMetaWithRealRun() throws IOException {
     // create commit instant
-    Files.createFile(Paths.get(tablePath, ".hoodie", "100.commit"));
+    FileCreateUtils.createCommit(HoodieCLI.getTableMetaClient(), "100");
 
     // create partition path
     String partition1 = Paths.get(tablePath, 
HoodieTestDataGenerator.DEFAULT_FIRST_PARTITION_PATH).toString();
@@ -205,7 +221,7 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
    * Test case for 'repair overwrite-hoodie-props'.
    */
   @Test
-  public void testOverwriteHoodieProperties() throws IOException {
+  public void testOverwriteHoodieProperties() throws Exception {
     URL newProps = 
this.getClass().getClassLoader().getResource("table-config.properties");
     assertNotNull(newProps, "New property file must exist");
 
@@ -227,19 +243,26 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
         .collect(Collectors.toMap(e -> String.valueOf(e.getKey()), e -> 
String.valueOf(e.getValue())));
     expected.putIfAbsent(TABLE_CHECKSUM.key(), 
String.valueOf(generateChecksum(tableConfig.getProps())));
     expected.putIfAbsent(DROP_PARTITION_COLUMNS.key(), 
String.valueOf(DROP_PARTITION_COLUMNS.defaultValue()));
+
+    // Properties that Hudi 1.x fills in on its own: the new-props file sets 
none of the three, so
+    // HoodieTableConfig.create writes its own defaults for the two paths and 
the command carries
+    // the initial table version over from the old properties. All three are 
fixed values here,
+    // never read back out of what the command wrote.
+    expected.putIfAbsent(HoodieTableConfig.TIMELINE_PATH.key(), 
HoodieTableConfig.TIMELINE_PATH.defaultValue());
+    expected.putIfAbsent(HoodieTableConfig.TIMELINE_HISTORY_PATH.key(), 
HoodieTableConfig.TIMELINE_HISTORY_PATH.defaultValue());
+    expected.putIfAbsent(HoodieTableConfig.INITIAL_VERSION.key(), 
String.valueOf(HoodieTableVersion.current().versionCode()));
+
     assertEquals(expected, result);
 
-    // check result
-    List<String> allPropsStr = Arrays.asList(NAME.key(), TYPE.key(), 
VERSION.key(),
-        TIMELINE_HISTORY_PATH.key(), TIMELINE_LAYOUT_VERSION.key(), 
TABLE_CHECKSUM.key(), DROP_PARTITION_COLUMNS.key());
-    String[][] rows = allPropsStr.stream().sorted().map(key -> new String[] 
{key,
-            oldProps.getOrDefault(key, "null"), result.getOrDefault(key, 
"null")})
+    // the rendered table lists one row per property, with its old and new 
value
+    TreeSet<String> allPropKeys = new TreeSet<>(oldProps.keySet());
+    allPropKeys.addAll(result.keySet());
+    String[][] rows = allPropKeys.stream()
+        .map(key -> new String[] {key, oldProps.getOrDefault(key, "null"), 
result.getOrDefault(key, "null")})
         .toArray(String[][]::new);
     String expect = HoodiePrintHelper.print(new String[] 
{HoodieTableHeaderFields.HEADER_HOODIE_PROPERTY,
         HoodieTableHeaderFields.HEADER_OLD_VALUE, 
HoodieTableHeaderFields.HEADER_NEW_VALUE}, rows);
-    expect = removeNonWordAndStripSpace(expect);
-    String got = removeNonWordAndStripSpace(cmdResult.toString());
-    assertEquals(expect, got);
+    assertEquals(removeNonWordAndStripSpace(expect), 
removeNonWordAndStripSpace(cmdResult.toString()));
   }
 
   /**
@@ -256,21 +279,186 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
     // Create four requested files
     for (int i = 100; i < 104; i++) {
       String timestamp = String.valueOf(i);
-      // Write corrupted requested Clean File
+      // Write an empty requested Clean File
       
HoodieTestCommitMetadataGenerator.createEmptyCleanRequestedFile(tablePath, 
timestamp, conf);
     }
 
+    // A plan cut short mid-write, so that its bytes stop inside the Avro 
header
+    FileCreateUtils.createRequestedCleanFile(metaClient, "104", 
validCleanerPlan());
+    truncateInHalf(metaClient, requestedCleanPath(metaClient, "104"));
+
+    // A plan whose writer was killed between opening and closing the Avro 
container, which leaves a
+    // complete header and no record behind
+    try (DataFileWriter<HoodieCleanerPlan> writer =
+             new DataFileWriter<>(new 
SpecificDatumWriter<>(HoodieCleanerPlan.class))) {
+      writer.create(HoodieCleanerPlan.getClassSchema(),
+          metaClient.getStorage().create(requestedCleanPath(metaClient, 
"105"), true));
+    }
+
+    // A plan that decodes, which the command has to leave in place
+    FileCreateUtils.createRequestedCleanFile(metaClient, "106", 
validCleanerPlan());
+
     // reload meta client
     metaClient = HoodieTableMetaClient.reload(metaClient);
-    // first, there are four instants
-    assertEquals(4, 
metaClient.getActiveTimeline().filterInflightsAndRequested().countInstants());
+    // first, there are seven pending instants
+    assertEquals(7, 
metaClient.getActiveTimeline().filterInflightsAndRequested().countInstants());
 
-    Object result = shell.evaluate(() -> "repair corrupted clean files");
-    assertTrue(ShellEvaluationResultUtil.isSuccess(result));
+    Object cleanResult = shell.evaluate(() -> "repair corrupted clean files");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(cleanResult));
 
     // reload meta client
     metaClient = HoodieTableMetaClient.reload(metaClient);
-    assertEquals(0, 
metaClient.getActiveTimeline().filterInflightsAndRequested().countInstants());
+    // the empty, the truncated and the record-less plans are gone and the 
readable one is untouched
+    List<HoodieInstant> remaining =
+        
metaClient.getActiveTimeline().filterInflightsAndRequested().getInstants();
+    assertEquals(1, remaining.size());
+    assertEquals("106", remaining.get(0).requestedTime());
+  }
+
+  /**
+   * A transient read failure on a valid pending clean plan must not be taken 
for corruption:
+   * the timeline serde wraps any exception raised while it streams the plan 
file in the same
+   * "unable to read commit metadata" IOException that an empty or truncated 
file raises.
+   */
+  @Test
+  public void testRemoveCorruptedPendingCleanActionKeepsPlanOnReadFailure() 
throws IOException {
+    HoodieCLI.conf = storageConf();
+    HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
+    FileCreateUtils.createRequestedCleanFile(metaClient, "100", 
validCleanerPlan());
+    StoragePath planPath = requestedCleanPath(metaClient, "100");
+
+    HoodieTableMetaClient timingOutClient = HoodieTableMetaClient.builder()
+        .setStorage(new TimingOutStorage(fs, 
planPath)).setBasePath(tablePath).build();
+    HoodieIOException thrown = assertThrows(HoodieIOException.class,
+        () -> 
RepairsCommand.removeCorruptedPendingCleanAction(timingOutClient));
+    assertInstanceOf(SocketTimeoutException.class, thrown.getCause());
+    assertTrue(metaClient.getStorage().exists(planPath));
+
+    // the same plan read through a healthy storage is left alone as well
+    
RepairsCommand.removeCorruptedPendingCleanAction(HoodieTableMetaClient.reload(metaClient));
+    assertEquals(1, 
HoodieTableMetaClient.reload(metaClient).getActiveTimeline()
+        .filterInflightsAndRequested().countInstants());
+  }
+
+  /**
+   * A clean that reached inflight keeps its plan in the requested file, so a 
corrupt plan has to
+   * take both files with it. The timeline collapses the two states into the 
inflight instant
+   * alone, so removing only the instant it listed would leave the corrupt 
plan behind for the
+   * next clean to fail on, and take a second run of this command to clear.
+   */
+  @Test
+  public void testRemoveCorruptedPendingCleanActionRemovesInflightAndItsPlan() 
throws IOException {
+    HoodieCLI.conf = storageConf();
+    HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
+
+    // a clean that was scheduled and started, whose plan was then cut short
+    FileCreateUtils.createRequestedCleanFile(metaClient, "100", 
validCleanerPlan());
+    // the inflight file a clean leaves behind carries no plan of its own
+    FileCreateUtils.createInflightCleanFile(metaClient, "100", null, true);
+    truncateInHalf(metaClient, requestedCleanPath(metaClient, "100"));
+
+    StoragePath inflightPath = new StoragePath(metaClient.getTimelinePath(),
+        
metaClient.getInstantFileNameGenerator().makeInflightCleanerFileName("100"));
+    assertTrue(metaClient.getStorage().exists(inflightPath));
+
+    metaClient = HoodieTableMetaClient.reload(metaClient);
+    List<HoodieInstant> pending = 
metaClient.getActiveTimeline().filterInflightsAndRequested().getInstants();
+    assertEquals(1, pending.size());
+    assertTrue(pending.get(0).isInflight());
+
+    RepairsCommand.removeCorruptedPendingCleanAction(metaClient);
+
+    // one pass takes the whole action, not just the file the timeline listed
+    assertFalse(metaClient.getStorage().exists(inflightPath));
+    assertFalse(metaClient.getStorage().exists(requestedCleanPath(metaClient, 
"100")));
+    assertEquals(0, 
HoodieTableMetaClient.reload(metaClient).getActiveTimeline()
+        .filterInflightsAndRequested().countInstants());
+  }
+
+  /**
+   * Corrupt bytes do not always reach the decoder as an Avro failure: a plan 
that decodes with no
+   * version leaves the migrator to unbox a null, and a length that survives 
as far as Avro's own
+   * ceiling raises an {@code UnsupportedOperationException}. Such an instant 
has to be judged
+   * corrupt like any other, and the instants behind it still repaired, rather 
than the failure
+   * escaping and abandoning the rest of the timeline.
+   */
+  @Test
+  public void 
testRemoveCorruptedPendingCleanActionRepairsPastAnUndecodablePlan() throws 
IOException {
+    HoodieCLI.conf = storageConf();
+    HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
+
+    HoodieCleanerPlan versionless = validCleanerPlan();
+    versionless.setVersion(null);
+    writeCleanerPlan(metaClient, "100", versionless);
+
+    // an ordinary corruption behind it, which is only reached if the first 
one does not escape
+    HoodieTestCommitMetadataGenerator.createEmptyCleanRequestedFile(tablePath, 
"101", HoodieCLI.conf);
+    // and a readable plan the command has to leave in place
+    FileCreateUtils.createRequestedCleanFile(metaClient, "102", 
validCleanerPlan());
+
+    metaClient = HoodieTableMetaClient.reload(metaClient);
+    assertEquals(3, 
metaClient.getActiveTimeline().filterInflightsAndRequested().countInstants());
+
+    RepairsCommand.removeCorruptedPendingCleanAction(metaClient);
+
+    List<HoodieInstant> remaining = HoodieTableMetaClient.reload(metaClient)
+        .getActiveTimeline().filterInflightsAndRequested().getInstants();
+    assertEquals(1, remaining.size());
+    assertEquals("102", remaining.get(0).requestedTime());
+  }
+
+  /**
+   * Cuts a file's bytes in half in place. The file is overwritten rather than 
deleted and
+   * rewritten, because both the emptiness check and the file name generator 
resolve the file
+   * from the instant itself.
+   */
+  private static void truncateInHalf(HoodieTableMetaClient metaClient, 
StoragePath path) throws IOException {
+    byte[] bytes;
+    try (InputStream in = metaClient.getStorage().open(path)) {
+      bytes = FileIOUtils.readAsByteArray(in);
+    }
+    try (OutputStream out = metaClient.getStorage().create(path, true)) {
+      out.write(bytes, 0, bytes.length / 2);
+    }
+  }
+
+  /**
+   * Writes a clean plan straight into the requested file of an instant, so 
that a plan the
+   * timeline's own writer would not produce still reaches the command.
+   */
+  private static void writeCleanerPlan(HoodieTableMetaClient metaClient, 
String instantTime,
+                                       HoodieCleanerPlan plan) throws 
IOException {
+    try (DataFileWriter<HoodieCleanerPlan> writer =
+             new DataFileWriter<>(new 
SpecificDatumWriter<>(HoodieCleanerPlan.class))) {
+      writer.create(HoodieCleanerPlan.getClassSchema(),
+          metaClient.getStorage().create(requestedCleanPath(metaClient, 
instantTime), true));
+      writer.append(plan);
+    }
+  }
+
+  /**
+   * The path of the requested file of a clean instant.
+   */
+  private static StoragePath requestedCleanPath(HoodieTableMetaClient 
metaClient, String instantTime) {
+    return new StoragePath(metaClient.getTimelinePath(),
+        
metaClient.getInstantFileNameGenerator().makeRequestedCleanerFileName(instantTime));
+  }
+
+  /**
+   * A clean plan that decodes into the latest plan version.
+   */
+  private static HoodieCleanerPlan validCleanerPlan() {
+    return HoodieCleanerPlan.newBuilder()
+        .setEarliestInstantToRetain(HoodieActionInstant.newBuilder()
+            .setAction(HoodieTimeline.COMMIT_ACTION).setTimestamp("001")
+            .setState(HoodieInstant.State.COMPLETED.name()).build())
+        
.setFilesToBeDeletedPerPartition(Collections.singletonMap("partition1", 
Collections.singletonList("file1")))
+        
.setFilePathsToBeDeletedPerPartition(Collections.singletonMap("partition1",
+            
Collections.singletonList(HoodieCleanFileInfo.newBuilder().setFilePath("file1").build())))
+        .setLastCompletedCommitTimestamp("002")
+        .setPolicy(HoodieCleaningPolicy.KEEP_LATEST_COMMITS.name())
+        .setVersion(CleanPlanV2MigrationHandler.VERSION)
+        .build();
   }
 
   /**
@@ -278,27 +466,33 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
    *
    */
   @Test
-  public void testShowFailedCommits() {
+  public void testShowFailedCommits() throws Exception {
     HoodieCLI.conf = storageConf();
 
     StorageConfiguration<?> conf = HoodieCLI.conf;
 
     HoodieTableMetaClient metaClient = HoodieCLI.getTableMetaClient();
 
+    // Every commit starts with real metadata, so none of them is empty yet.
     for (int i = 1; i < 20; i++) {
-      String timestamp = String.valueOf(i);
-      // Write corrupted requested Clean File
-      HoodieTestCommitMetadataGenerator.createCommitFile(tablePath, timestamp, 
conf);
+      
HoodieTestCommitMetadataGenerator.createCommitFileWithMetadata(tablePath, 
String.valueOf(i), conf);
     }
 
-    metaClient.getActiveTimeline().getInstantsAsStream().filter(hoodieInstant 
-> Integer.parseInt(hoodieInstant.requestedTime()) % 4 == 
0).forEach(hoodieInstant -> {
-      metaClient.getActiveTimeline().deleteInstantFileIfExists(hoodieInstant);
-      if (hoodieInstant.isCompleted()) {
-        metaClient.getActiveTimeline().createCompleteInstant(hoodieInstant);
-      } else {
-        metaClient.getActiveTimeline().createNewInstant(hoodieInstant);
-      }
-    });
+    HoodieTableMetaClient reloaded = HoodieTableMetaClient.reload(metaClient);
+    // Truncate a subset in place. Rewriting via createCompleteInstant would 
mint a fresh
+    // completion time and leave the instant name pointing at a file that no 
longer exists.
+    List<HoodieInstant> allInstants = 
reloaded.getActiveTimeline().getInstantsAsStream().collect(Collectors.toList());
+    List<HoodieInstant> emptied = allInstants.stream()
+        .filter(instant -> Integer.parseInt(instant.requestedTime()) % 4 == 0)
+        .collect(Collectors.toList());
+    for (HoodieInstant instant : emptied) {
+      Path instantPath = new Path(reloaded.getTimelinePath().toString(),
+          reloaded.getInstantFileNameGenerator().getFileName(instant));
+      HoodieTestDataGenerator.createEmptyFile(tablePath, instantPath, conf);
+    }
+    // A proper subset, so the command has to filter rather than report 
everything.
+    assertTrue(emptied.size() > 0 && emptied.size() < allInstants.size(),
+        "truncated " + emptied.size() + " of " + allInstants.size() + " 
instants; expected a proper subset");
 
     final TestLogAppender appender = new TestLogAppender();
     final Logger logger = (Logger) LogManager.getLogger(RepairsCommand.class);
@@ -308,7 +502,8 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
       Object result = shell.evaluate(() -> "repair show empty commit 
metadata");
       assertTrue(ShellEvaluationResultUtil.isSuccess(result));
       final List<LogEvent> log = appender.getLog();
-      assertEquals(log.size(),4);
+      // only the instants truncated above should be flagged as empty
+      assertEquals(emptied.size(), log.size());
       log.forEach(LoggingEvent -> {
         assertEquals(LoggingEvent.getLevel(), Level.WARN);
         
assertTrue(LoggingEvent.getMessage().getFormattedMessage().contains("Empty 
Commit: "));
@@ -332,6 +527,7 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
         .setPartitionFields("partition_path")
         .setRecordKeyFields("_row_key")
         .setKeyGeneratorClassProp(SimpleKeyGenerator.class.getCanonicalName())
+        .setTableVersion(HoodieTableVersion.current().versionCode())
         .initTable(HoodieCLI.conf.newInstance(), tablePath);
 
     HoodieTestDataGenerator dataGen = new HoodieTestDataGenerator();
@@ -346,6 +542,7 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
       JavaRDD<HoodieRecord> writeRecords = 
context().getJavaSparkContext().parallelize(records, 1);
       List<WriteStatus> result = client.upsert(writeRecords, 
newCommitTime).collect();
       Assertions.assertNoWriteErrors(result);
+      client.commit(newCommitTime, jsc().parallelize(result));
 
       newCommitTime = "002";
       // Generate HoodieRecords w/ null values for partition path field.
@@ -363,6 +560,7 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
       JavaRDD<HoodieRecord> writeRecords2 = 
context().getJavaSparkContext().parallelize(records2, 1);
       List<WriteStatus> result2 = client.bulkInsert(writeRecords2, 
newCommitTime).collect();
       Assertions.assertNoWriteErrors(result2);
+      client.commit(newCommitTime, jsc().parallelize(result2));
 
       SQLContext sqlContext = context().getSqlContext();
       long totalRecs = 
sqlContext.read().format("hudi").load(tablePath).count();
@@ -394,6 +592,7 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
         .setPartitionFields("partition_path")
         .setRecordKeyFields("_row_key")
         .setKeyGeneratorClassProp(SimpleKeyGenerator.class.getCanonicalName())
+        .setTableVersion(HoodieTableVersion.current().versionCode())
         .initTable(HoodieCLI.conf.newInstance(), tablePath);
 
     HoodieTestDataGenerator dataGen = new HoodieTestDataGenerator();
@@ -408,12 +607,15 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
       JavaRDD<HoodieRecord> writeRecords = 
context().getJavaSparkContext().parallelize(records, 1);
       List<WriteStatus> result = client.upsert(writeRecords, 
newCommitTime).collect();
       Assertions.assertNoWriteErrors(result);
+      client.commit(newCommitTime, jsc().parallelize(result));
 
       SQLContext sqlContext = context().getSqlContext();
       long totalRecs = 
sqlContext.read().format("hudi").load(tablePath).count();
       assertEquals(totalRecs, 20);
       long totalRecsInOldPartition = 
sqlContext.read().format("hudi").load(tablePath)
           .filter(HoodieRecord.PARTITION_PATH_METADATA_FIELD + " == '" + 
DEFAULT_FIRST_PARTITION_PATH + "'").count();
+      // otherwise the final assertion below would be satisfied by 0 == 0
+      assertTrue(totalRecsInOldPartition > 0);
 
       // Execute rename partition command
       assertEquals(0, SparkMain.renamePartition(jsc(), tablePath, 
DEFAULT_FIRST_PARTITION_PATH, "2016/03/18"));
@@ -430,6 +632,51 @@ public class TestRepairsCommand extends 
CLIFunctionalTestHarness {
     }
   }
 
+  /**
+   * Storage whose read of one file times out part-way, the way a remote store 
does under a
+   * transient outage: the open succeeds and the stream fails after the first 
bytes.
+   */
+  private static class TimingOutStorage extends HoodieHadoopStorage {
+    private final StoragePath timingOutPath;
+
+    TimingOutStorage(FileSystem fs, StoragePath timingOutPath) {
+      super(fs);
+      this.timingOutPath = timingOutPath;
+    }
+
+    @Override
+    public InputStream open(StoragePath path) throws IOException {
+      InputStream in = super.open(path);
+      if (!path.equals(timingOutPath)) {
+        return in;
+      }
+      return new FilterInputStream(in) {
+        private int remaining = 32;
+
+        @Override
+        public int read() throws IOException {
+          if (remaining <= 0) {
+            throw new SocketTimeoutException("Read timed out");
+          }
+          remaining--;
+          return super.read();
+        }
+
+        @Override
+        public int read(byte[] buffer, int offset, int length) throws 
IOException {
+          if (remaining <= 0) {
+            throw new SocketTimeoutException("Read timed out");
+          }
+          int read = super.read(buffer, offset, Math.min(length, remaining));
+          if (read > 0) {
+            remaining -= read;
+          }
+          return read;
+        }
+      };
+    }
+  }
+
   class TestLogAppender extends AbstractAppender {
     private final List<LogEvent> log = new ArrayList<>();
 
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRestoresCommand.java 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRestoresCommand.java
index 0ff5b3b428b5..982593bbd1d1 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRestoresCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRestoresCommand.java
@@ -31,17 +31,14 @@ import org.apache.hudi.client.SparkRDDWriteClient;
 import org.apache.hudi.common.config.HoodieMetadataConfig;
 import org.apache.hudi.common.model.HoodieTableType;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.table.timeline.HoodieActiveTimeline;
 import org.apache.hudi.common.table.timeline.HoodieInstant;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
-import org.apache.hudi.common.testutils.HoodieMetadataTestTable;
 import org.apache.hudi.common.testutils.HoodieTestTable;
 import org.apache.hudi.common.util.Option;
 import org.apache.hudi.config.HoodieIndexConfig;
 import org.apache.hudi.config.HoodieWriteConfig;
 import org.apache.hudi.index.HoodieIndex;
-import org.apache.hudi.metadata.HoodieTableMetadataWriter;
-import org.apache.hudi.metadata.SparkHoodieBackedTableMetadataWriter;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Tag;
@@ -52,17 +49,17 @@ import org.springframework.shell.Shell;
 
 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.stream.Stream;
+import java.util.stream.Collectors;
 
 import static 
org.apache.hudi.common.testutils.HoodieTestDataGenerator.DEFAULT_FIRST_PARTITION_PATH;
 import static 
org.apache.hudi.common.testutils.HoodieTestDataGenerator.DEFAULT_PARTITION_PATHS;
 import static 
org.apache.hudi.common.testutils.HoodieTestDataGenerator.DEFAULT_SECOND_PARTITION_PATH;
 import static 
org.apache.hudi.common.testutils.HoodieTestDataGenerator.DEFAULT_THIRD_PARTITION_PATH;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @Tag("functional")
@@ -78,7 +75,7 @@ public class TestRestoresCommand extends 
CLIFunctionalTestHarness {
     String tablePath = tablePath(tableName);
     new TableCommand().createTable(
             tablePath, tableName, HoodieTableType.MERGE_ON_READ.name(),
-            "", TimelineLayoutVersion.VERSION_1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+            "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
     HoodieTableMetaClient metaClient = 
HoodieTableMetaClient.reload(HoodieCLI.getTableMetaClient());
     //Create some commits files and base files
     Map<String, String> partitionAndFileId = new HashMap<String, String>() {
@@ -91,55 +88,65 @@ public class TestRestoresCommand extends 
CLIFunctionalTestHarness {
 
     HoodieWriteConfig config = 
HoodieWriteConfig.newBuilder().withPath(tablePath)
             .withMetadataConfig(
-                    // Column Stats Index is disabled, since these tests 
construct tables which are
+                    // The metadata table is disabled, since these tests 
construct tables which are
                     // not valid (empty commit metadata, etc)
                     HoodieMetadataConfig.newBuilder()
-                            .withMetadataIndexColumnStats(false)
+                            .enable(false)
                             .build()
             )
             .withRollbackUsingMarkers(false)
             
.withIndexConfig(HoodieIndexConfig.newBuilder().withIndexType(HoodieIndex.IndexType.INMEMORY).build())
             .build();
 
-    try (HoodieTableMetadataWriter metadataWriter = 
SparkHoodieBackedTableMetadataWriter.create(metaClient.getStorageConf(), 
config, context)) {
-      HoodieTestTable hoodieTestTable = HoodieMetadataTestTable.of(metaClient, 
metadataWriter, Option.of(context))
-          .withPartitionMetaFiles(DEFAULT_PARTITION_PATHS)
-          .addCommit("100")
-          .withBaseFilesInPartitions(partitionAndFileId).getLeft()
-          .addCommit("101");
+    HoodieTestTable hoodieTestTable = HoodieTestTable.of(metaClient)
+        .withPartitionMetaFiles(DEFAULT_PARTITION_PATHS)
+        .addCommit("100", Option.of("100001"), Option.empty())
+        .withBaseFilesInPartitions(partitionAndFileId).getLeft()
+        .addCommit("101", Option.of("101001"), Option.empty());
 
-      
hoodieTestTable.addCommit("102").withBaseFilesInPartitions(partitionAndFileId);
-      HoodieSavepointMetadata savepointMetadata2 = 
hoodieTestTable.doSavepoint("102");
-      hoodieTestTable.addSavepoint("102", savepointMetadata2);
+    hoodieTestTable.addCommit("102", Option.of("102001"), 
Option.empty()).withBaseFilesInPartitions(partitionAndFileId);
+    HoodieSavepointMetadata savepointMetadata2 = 
hoodieTestTable.doSavepoint("102");
+    hoodieTestTable.addSavepointCommit("102", Option.of("102002"), 
savepointMetadata2);
 
-      
hoodieTestTable.addCommit("103").withBaseFilesInPartitions(partitionAndFileId);
+    hoodieTestTable.addCommit("103", Option.of("103001"), 
Option.empty()).withBaseFilesInPartitions(partitionAndFileId);
 
-      try (BaseHoodieWriteClient client = new SparkRDDWriteClient(context(), 
config)) {
-        client.rollback("103");
-        client.restoreToSavepoint("102");
+    try (BaseHoodieWriteClient client = new SparkRDDWriteClient(context(), 
config)) {
+      client.rollback("103");
+      client.restoreToSavepoint("102");
+    }
 
-        
hoodieTestTable.addCommit("105").withBaseFilesInPartitions(partitionAndFileId);
-        HoodieSavepointMetadata savepointMetadata = 
hoodieTestTable.doSavepoint("105");
-        hoodieTestTable.addSavepoint("105", savepointMetadata);
+    hoodieTestTable.addCommit("105", Option.of("105001"), 
Option.empty()).withBaseFilesInPartitions(partitionAndFileId);
+    HoodieSavepointMetadata savepointMetadata = 
hoodieTestTable.doSavepoint("105");
+    hoodieTestTable.addSavepointCommit("105", Option.of("105002"), 
savepointMetadata);
 
-        
hoodieTestTable.addCommit("106").withBaseFilesInPartitions(partitionAndFileId);
-        client.rollback("106");
-        client.restoreToSavepoint("105");
-      }
+    hoodieTestTable.addCommit("106", Option.of("106001"), 
Option.empty()).withBaseFilesInPartitions(partitionAndFileId);
+
+    try (BaseHoodieWriteClient client = new SparkRDDWriteClient(context(), 
config)) {
+      client.restoreToSavepoint("105");
     }
   }
 
   @Test
-  public void testShowRestores() {
+  public void testShowRestores() throws IOException {
     Object result = shell.evaluate(() -> "show restores");
     assertTrue(ShellEvaluationResultUtil.isSuccess(result));
 
     // get restored instants
     HoodieActiveTimeline activeTimeline = 
HoodieCLI.getTableMetaClient().getActiveTimeline();
-    Stream<HoodieInstant> restores = 
activeTimeline.getRestoreTimeline().filterCompletedInstants().getInstantsAsStream();
+    List<HoodieInstant> restores = 
activeTimeline.getRestoreTimeline().filterCompletedInstants()
+            .getInstantsAsStream().sorted().collect(Collectors.toList());
+    // init() runs exactly two restores: restoreToSavepoint("102") and 
restoreToSavepoint("105")
+    assertEquals(2, restores.size());
+    // The restore to savepoint 102 still rolls back commit 103: the preceding 
client.rollback("103")
+    // only un-publishes it, leaving 103 behind as an inflight commit for the 
restore to clean up
+    assertEquals(Collections.singletonList("103"),
+            
activeTimeline.readRestoreMetadata(restores.get(0)).getInstantsToRollback());
+    // The restore to savepoint 105 rolls back commit 106, the only commit 
written after that savepoint
+    assertEquals(Collections.singletonList("106"),
+            
activeTimeline.readRestoreMetadata(restores.get(1)).getInstantsToRollback());
 
     List<Comparable[]> rows = new ArrayList<>();
-    restores.sorted().forEach(instant -> {
+    restores.forEach(instant -> {
       try {
         HoodieRestoreMetadata metadata = 
activeTimeline.readRestoreMetadata(instant);
         metadata.getInstantsToRollback().forEach(c -> {
@@ -155,6 +162,9 @@ public class TestRestoresCommand extends 
CLIFunctionalTestHarness {
       }
     });
 
+    // One row per rolled back instant, one from each of the two restores
+    assertEquals(2, rows.size());
+
     TableHeader header = new TableHeader()
             .addTableHeaderField(HoodieTableHeaderFields.HEADER_INSTANT)
             
.addTableHeaderField(HoodieTableHeaderFields.HEADER_RESTORE_INSTANT)
@@ -171,15 +181,20 @@ public class TestRestoresCommand extends 
CLIFunctionalTestHarness {
   public void testShowRestore() throws IOException {
     // get instant
     HoodieActiveTimeline activeTimeline = 
HoodieCLI.getTableMetaClient().getActiveTimeline();
-    Stream<HoodieInstant> restores = 
activeTimeline.getRestoreTimeline().filterCompletedInstants().getInstantsAsStream();
-    HoodieInstant instant = restores.findFirst().orElse(null);
-    assertNotNull(instant, "The instant can not be null.");
+    List<HoodieInstant> restores = 
activeTimeline.getRestoreTimeline().filterCompletedInstants()
+            .getInstantsAsStream().sorted().collect(Collectors.toList());
+    // init() runs exactly two restores: restoreToSavepoint("102") and 
restoreToSavepoint("105")
+    assertEquals(2, restores.size());
+    HoodieInstant instant = restores.get(0);
 
     Object result = shell.evaluate(() -> "show restore --instant " + 
instant.requestedTime());
     assertTrue(ShellEvaluationResultUtil.isSuccess(result));
 
     // get metadata of instant
     HoodieRestoreMetadata instantMetadata = 
activeTimeline.readRestoreMetadata(instant);
+    // The earliest of the two restores done by init() is the one to savepoint 
102, and it rolls back
+    // commit 103, which the preceding client.rollback("103") only 
un-published rather than removed
+    assertEquals(Collections.singletonList("103"), 
instantMetadata.getInstantsToRollback());
 
     // generate expected result
     TableHeader header = new TableHeader()
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRollbacksCommand.java 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRollbacksCommand.java
index baf5db208cea..951213b856ed 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRollbacksCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestRollbacksCommand.java
@@ -30,9 +30,9 @@ import org.apache.hudi.client.SparkRDDWriteClient;
 import org.apache.hudi.common.config.HoodieMetadataConfig;
 import org.apache.hudi.common.model.HoodieTableType;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.table.timeline.HoodieActiveTimeline;
 import org.apache.hudi.common.table.timeline.HoodieInstant;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
 import org.apache.hudi.common.testutils.HoodieMetadataTestTable;
 import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.collection.Pair;
@@ -80,7 +80,7 @@ public class TestRollbacksCommand extends 
CLIFunctionalTestHarness {
     String tablePath = tablePath(tableName);
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.MERGE_ON_READ.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
     HoodieTableMetaClient metaClient = 
HoodieTableMetaClient.reload(HoodieCLI.getTableMetaClient());
     //Create some commits files and base files
     Map<String, String> partitionAndFileId = new HashMap<String, String>() {
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestSavepointsCommand.java
 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestSavepointsCommand.java
index abc9a6141d19..4700b2366319 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestSavepointsCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestSavepointsCommand.java
@@ -24,8 +24,8 @@ import org.apache.hudi.cli.HoodieTableHeaderFields;
 import org.apache.hudi.cli.functional.CLIFunctionalTestHarness;
 import org.apache.hudi.cli.testutils.ShellEvaluationResultUtil;
 import org.apache.hudi.common.model.HoodieTableType;
+import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.table.timeline.HoodieTimeline;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
 import org.apache.hudi.common.testutils.HoodieTestDataGenerator;
 
 import org.junit.jupiter.api.BeforeEach;
@@ -62,7 +62,7 @@ public class TestSavepointsCommand extends 
CLIFunctionalTestHarness {
     // Create table and connect
     new TableCommand().createTable(
         tablePath, "test_table", HoodieTableType.COPY_ON_WRITE.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
   }
 
   /**
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestStatsCommand.java 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestStatsCommand.java
index 8558d4dd4c67..58e47805b1ea 100644
--- a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestStatsCommand.java
+++ b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestStatsCommand.java
@@ -26,7 +26,7 @@ import 
org.apache.hudi.cli.functional.CLIFunctionalTestHarness;
 import org.apache.hudi.cli.testutils.HoodieTestCommitMetadataGenerator;
 import org.apache.hudi.cli.testutils.ShellEvaluationResultUtil;
 import org.apache.hudi.common.model.HoodieTableType;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
+import org.apache.hudi.common.table.HoodieTableVersion;
 import org.apache.hudi.common.testutils.HoodieTestDataGenerator;
 import org.apache.hudi.common.testutils.HoodieTestTable;
 import org.apache.hudi.common.util.Option;
@@ -73,7 +73,7 @@ public class TestStatsCommand extends 
CLIFunctionalTestHarness {
     // Create table and connect
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.COPY_ON_WRITE.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
   }
 
   /**
diff --git 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestUpgradeDowngradeCommand.java
 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestUpgradeDowngradeCommand.java
index 0aeb3baa1765..f38406cbcc04 100644
--- 
a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestUpgradeDowngradeCommand.java
+++ 
b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestUpgradeDowngradeCommand.java
@@ -27,7 +27,6 @@ import org.apache.hudi.common.model.IOType;
 import org.apache.hudi.common.table.HoodieTableConfig;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
 import org.apache.hudi.common.table.HoodieTableVersion;
-import org.apache.hudi.common.table.timeline.versioning.TimelineLayoutVersion;
 import org.apache.hudi.common.table.view.FileSystemViewStorageConfig;
 import org.apache.hudi.common.testutils.FileCreateUtilsLegacy;
 import org.apache.hudi.common.testutils.HoodieTestTable;
@@ -70,7 +69,7 @@ public class TestUpgradeDowngradeCommand extends 
CLIFunctionalTestHarness {
     tablePath = tablePath(tableName);
     new TableCommand().createTable(
         tablePath, tableName, HoodieTableType.COPY_ON_WRITE.name(),
-        "", TimelineLayoutVersion.VERSION_1, 
"org.apache.hudi.common.model.HoodieAvroPayload");
+        "", HoodieTableVersion.current().versionCode(), 
"org.apache.hudi.common.model.HoodieAvroPayload");
     timelineService = HoodieClientTestUtils.initTimelineService(
         context, basePath(), 
FileSystemViewStorageConfig.REMOTE_PORT_NUM.defaultValue());
     metaClient = HoodieTableMetaClient.reload(HoodieCLI.getTableMetaClient());

Reply via email to