provide .hg_sync meta data;
authorwenzelm
Sun, 05 Jun 2022 19:19:55 +0200
changeset 75511 b32fdb67f851
parent 75510 0106c89fb71f
child 75512 2251548ec4a8
provide .hg_sync meta data; always clean target, but guard it wrt. .hg_sync for robustness (no need for dry-run); updated and clarified documentation;
src/Doc/System/Misc.thy
src/Pure/Admin/sync_repos.scala
src/Pure/General/mercurial.scala
src/Pure/System/isabelle_system.scala
--- a/src/Doc/System/Misc.thy	Sat Jun 04 16:54:24 2022 +0200
+++ b/src/Doc/System/Misc.thy	Sun Jun 05 19:19:55 2022 +0200
@@ -304,20 +304,17 @@
 
 text \<open>
   The @{tool_def hg_sync} tool synchronizes a local Mercurial repository with
-  a target directory, using \<^verbatim>\<open>rsync\<close>\<^footnote>\<open>\<^url>\<open>https://linux.die.net/man/1/rsync\<close>\<close>
-  notation for destinations.
+  a target directory.
 
   @{verbatim [display]
 \<open>Usage: isabelle hg_sync [OPTIONS] TARGET
 
   Options are:
-    -C           clean all unknown/ignored files on target
-                 (implies -n for testing; use option -f to confirm)
-    -F RULE      add rsync filter RULE (e.g. "protect /foo" to avoid deletion)
+    -F RULE      add rsync filter RULE
+                 (e.g. "protect /foo" to avoid deletion)
     -R ROOT      explicit repository root directory
                  (default: implicit from current directory)
     -T           thorough treatment of file content and directory times
-    -f           force changes: no dry-run
     -n           no changes: dry-run
     -r REV       explicit revision (default: state of working directory)
     -p PORT      explicit SSH port (default: 22)
@@ -327,21 +324,21 @@
   which can be local or remote (using notation of rsync).\<close>}
 
   The \<^verbatim>\<open>TARGET\<close> specification can be a local or remote directory (via ssh),
-  using \<^verbatim>\<open>rsync\<close> notation (see examples below). The content is written
-  directly into the target, \<^emph>\<open>without\<close> creating a separate sub-directory.
+  using \<^verbatim>\<open>rsync\<close>\<^footnote>\<open>\<^url>\<open>https://linux.die.net/man/1/rsync\<close>\<close> notation for
+  destinations; see also examples below. The content is written directly into
+  the target, \<^emph>\<open>without\<close> creating a separate sub-directory. The special
+  sub-directory \<^verbatim>\<open>.hg_sync\<close> within the target contains meta data from the
+  original Mercurial repository. Repeated synchronization is guarded by the
+  presence of a \<^verbatim>\<open>.hg_sync\<close> sub-directory: this sanity check prevents
+  accidental changes (or deletion!) of targets that were not created by @{tool
+  hg_sync}.
 
   \<^medskip> Option \<^verbatim>\<open>-r\<close> specifies an explicit revision of the repository; the default
   is the current state of the working directory (which might be uncommitted).
 
   \<^medskip> Option \<^verbatim>\<open>-v\<close> enables verbose mode. Option \<^verbatim>\<open>-n\<close> enables ``dry-run'' mode:
-  operations are only simulated and printed as in verbose mode. Option \<^verbatim>\<open>-f\<close>
-  disables ``dry-run'' mode and thus forces changes to be applied.
-
-  \<^medskip> Option \<^verbatim>\<open>-C\<close> causes deletion of all unknown/ignored files on the target.
-  This is potentially dangerous: giving a wrong target directory will cause
-  its total destruction! For robustness, option \<^verbatim>\<open>-C\<close> implies option \<^verbatim>\<open>-n\<close>,
-  for ``dry-run'' with verbose output. A subsequent option \<^verbatim>\<open>-f\<close> is required
-  to force actual deletions on the target.
+  operations are only simulated; use it with option \<^verbatim>\<open>-v\<close> to actually see
+  results.
 
   \<^medskip> Option \<^verbatim>\<open>-F\<close> adds a filter rule to the underlying \<^verbatim>\<open>rsync\<close> command;
   multiple \<^verbatim>\<open>-F\<close> options may be given to accumulate a list of rules.
@@ -363,10 +360,7 @@
 text \<open>
   Synchronize the current repository onto a remote host, with accurate
   treatment of all content:
-  @{verbatim [display] \<open>  isabelle hg_sync -T -C remotename:test/repos\<close>}
-
-  So far, this is only a dry run. In a realistic situation, it requires
-  consecutive options \<^verbatim>\<open>-C -f\<close> as confirmation.
+  @{verbatim [display] \<open>  isabelle hg_sync -T remotename:test/repos\<close>}
 \<close>
 
 
--- a/src/Pure/Admin/sync_repos.scala	Sat Jun 04 16:54:24 2022 +0200
+++ b/src/Pure/Admin/sync_repos.scala	Sun Jun 05 19:19:55 2022 +0200
@@ -15,38 +15,30 @@
     thorough: Boolean = false,
     preserve_jars: Boolean = false,
     dry_run: Boolean = false,
-    clean: Boolean = false,
     rev: String = "",
     afp_root: Option[Path] = None,
     afp_rev: String = ""
   ): Unit = {
-    val target_dir = if (target.endsWith(":") || target.endsWith("/")) target else target + "/"
-
     val hg = Mercurial.self_repository()
     val afp_hg = afp_root.map(Mercurial.repository(_))
 
     val more_filter = if (preserve_jars) List("include *.jar", "protect *.jar") else Nil
 
-    def sync(hg: Mercurial.Repository, dest: String, r: String, filter: List[String] = Nil): Unit =
-      hg.sync(dest, rev = r, progress = progress, port = port, verbose = verbose || dry_run,
-        thorough = thorough, dry_run = dry_run, clean = clean, filter = filter ::: more_filter)
+    def sync(hg: Mercurial.Repository, dest: String, r: String,
+      contents: List[File.Content] = Nil, filter: List[String] = Nil
+    ): Unit = {
+      hg.sync(dest, rev = r, progress = progress, port = port, verbose = verbose,
+        thorough = thorough, dry_run = dry_run, contents = contents, filter = filter ::: more_filter)
+    }
 
     progress.echo("\n* Isabelle repository:")
-    sync(hg, target, rev, filter = List("protect /AFP", "protect /etc/ISABELLE_ID"))
-
-    if (!dry_run) {
-      Isabelle_System.with_tmp_dir("sync") { tmp_dir =>
-        val id_path = tmp_dir + Path.explode("ISABELLE_ID")
-        File.write(id_path, hg.id(rev = rev))
-        Isabelle_System.rsync(port = port, thorough = thorough,
-          args = List(File.standard_path(id_path), target_dir + "etc/")
-        ).check
-      }
-    }
+    sync(hg, target, rev,
+      contents = List(File.Content(Path.explode("etc/ISABELLE_ID"), hg.id(rev = rev))),
+      filter = List("protect /AFP"))
 
     for (hg <- afp_hg) {
       progress.echo("\n* AFP repository:")
-      sync(hg, target_dir + "AFP", afp_rev)
+      sync(hg, Isabelle_System.rsync_dir(target) + "/AFP", afp_rev)
     }
   }
 
@@ -54,7 +46,6 @@
     Isabelle_Tool("sync_repos", "synchronize Isabelle + AFP repositories",
       Scala_Project.here, { args =>
         var afp_root: Option[Path] = None
-        var clean = false
         var preserve_jars = false
         var thorough = false
         var afp_rev = ""
@@ -69,11 +60,8 @@
   Options are:
     -A ROOT      include AFP with given root directory (":" for """ + AFP.BASE.implode + """)
     -J           preserve *.jar files
-    -C           clean all unknown/ignored files on target
-                 (implies -n for testing; use option -f to confirm)
     -T           thorough treatment of file content and directory times
     -a REV       explicit AFP revision (default: state of working directory)
-    -f           force changes: no dry-run
     -n           no changes: dry-run
     -r REV       explicit revision (default: state of working directory)
     -p PORT      explicit SSH port (default: """ + SSH.default_port + """)
@@ -81,22 +69,18 @@
 
   Synchronize Isabelle + AFP repositories; see also "isabelle hg_sync".
 
-  Examples (without -f as "dry-run"):
+  Example: quick testing
 
-   * quick testing
+    isabelle sync_repos -A: -J testmachine:test/isabelle_afp
 
-      isabelle sync_repos -A: -J -C testmachine:test/isabelle_afp
+  Example: accurate testing
 
-   * accurate testing
-
-      isabelle sync_repos -A: -T -C testmachine:test/isabelle_afp
+    isabelle sync_repos -A: -T testmachine:test/isabelle_afp
 """,
           "A:" -> (arg => afp_root = Some(if (arg == ":") AFP.BASE else Path.explode(arg))),
           "J" -> (_ => preserve_jars = true),
-          "C" -> { _ => clean = true; dry_run = true },
           "T" -> (_ => thorough = true),
           "a:" -> (arg => afp_rev = arg),
-          "f" -> (_ => dry_run = false),
           "n" -> (_ => dry_run = true),
           "r:" -> (arg => rev = arg),
           "p:" -> (arg => port = Value.Int.parse(arg)),
@@ -111,8 +95,8 @@
 
         val progress = new Console_Progress
         sync_repos(target, progress = progress, port = port, verbose = verbose, thorough = thorough,
-          preserve_jars = preserve_jars, dry_run = dry_run, clean = clean, rev = rev,
-          afp_root = afp_root, afp_rev = afp_rev)
+          preserve_jars = preserve_jars, dry_run = dry_run, rev = rev, afp_root = afp_root,
+          afp_rev = afp_rev)
       }
     )
 }
--- a/src/Pure/General/mercurial.scala	Sat Jun 04 16:54:24 2022 +0200
+++ b/src/Pure/General/mercurial.scala	Sun Jun 05 19:19:55 2022 +0200
@@ -115,6 +115,12 @@
     archive_info(root).map(info => info.tags.mkString(" "))
 
 
+  /* hg_sync meta data */
+
+  val HG_SYNC: Path = Path.explode(".hg_sync")
+  val HG_SYNC_ID: Path = HG_SYNC + Path.explode("id")
+
+
   /* repository access */
 
   def is_repository(root: Path, ssh: SSH.System = SSH.Local): Boolean =
@@ -262,13 +268,26 @@
       verbose: Boolean = false,
       thorough: Boolean = false,
       dry_run: Boolean = false,
-      clean: Boolean = false,
       filter: List[String] = Nil,
+      contents: List[File.Content] = Nil,
       rev: String = ""
     ): Unit = {
       require(ssh == SSH.Local, "local repository required")
 
       Isabelle_System.with_tmp_dir("sync") { tmp_dir =>
+        Isabelle_System.rsync_init(target, port = port)
+
+        val list =
+          Isabelle_System.rsync(port = port, list = true,
+            args = List("--", Isabelle_System.rsync_dir(target))
+          ).check.out_lines.filterNot(_.endsWith(" ."))
+        if (list.nonEmpty && !list.exists(_.endsWith(" .hg_sync"))) {
+          error("No .hg_sync meta data in " + quote(target))
+        }
+
+        Isabelle_System.rsync_init(target, port = port,
+          contents = File.Content(HG_SYNC_ID, id(rev = rev)) :: contents)
+
         val (options, source) =
           if (rev.isEmpty) {
             val exclude_path = tmp_dir + Path.explode("exclude")
@@ -284,10 +303,16 @@
             archive(source, rev = rev)
             (Nil, source)
           }
+
+        val protect =
+          (HG_SYNC :: contents.map(_.path)).map(path => "protect /" + File.standard_path(path))
         Isabelle_System.rsync(
           progress = progress, port = port, verbose = verbose, thorough = thorough,
-          dry_run = dry_run, clean = clean, filter = filter,
-          args = List("--prune-empty-dirs") ::: options ::: List("--", source + "/.", target)
+          dry_run = dry_run,
+          clean = true,
+          prune_empty_dirs = true,
+          filter = protect ::: filter,
+          args = options ::: List("--", Isabelle_System.rsync_dir(source), target)
         ).check
       }
     }
@@ -492,7 +517,6 @@
   val isabelle_tool2 =
     Isabelle_Tool("hg_sync", "synchronize Mercurial repository with target directory",
       Scala_Project.here, { args =>
-        var clean = false
         var filter: List[String] = Nil
         var root: Option[Path] = None
         var thorough = false
@@ -505,13 +529,11 @@
 Usage: isabelle hg_sync [OPTIONS] TARGET
 
   Options are:
-    -C           clean all unknown/ignored files on target
-                 (implies -n for testing; use option -f to confirm)
-    -F RULE      add rsync filter RULE (e.g. "protect /foo" to avoid deletion)
+    -F RULE      add rsync filter RULE
+                 (e.g. "protect /foo" to avoid deletion)
     -R ROOT      explicit repository root directory
                  (default: implicit from current directory)
     -T           thorough treatment of file content and directory times
-    -f           force changes: no dry-run
     -n           no changes: dry-run
     -r REV       explicit revision (default: state of working directory)
     -p PORT      explicit SSH port (default: """ + SSH.default_port + """)
@@ -520,11 +542,9 @@
   Synchronize Mercurial repository with TARGET directory,
   which can be local or remote (using notation of rsync).
 """,
-          "C" -> { _ => clean = true; dry_run = true },
           "F:" -> (arg => filter = filter ::: List(arg)),
           "R:" -> (arg => root = Some(Path.explode(arg))),
           "T" -> (_ => thorough = true),
-          "f" -> (_ => dry_run = false),
           "n" -> (_ => dry_run = true),
           "r:" -> (arg => rev = arg),
           "p:" -> (arg => port = Value.Int.parse(arg)),
@@ -543,8 +563,8 @@
             case Some(dir) => repository(dir)
             case None => the_repository(Path.current)
           }
-        hg.sync(target, progress = progress, port = port, verbose = verbose || dry_run,
-          thorough = thorough, dry_run = dry_run, clean = clean, filter = filter, rev = rev)
+        hg.sync(target, progress = progress, port = port, verbose = verbose, thorough = thorough,
+          dry_run = dry_run, filter = filter, rev = rev)
       }
     )
 }
--- a/src/Pure/System/isabelle_system.scala	Sat Jun 04 16:54:24 2022 +0200
+++ b/src/Pure/System/isabelle_system.scala	Sun Jun 05 19:19:55 2022 +0200
@@ -426,8 +426,10 @@
     port: Int = SSH.default_port,
     verbose: Boolean = false,
     thorough: Boolean = false,
+    prune_empty_dirs: Boolean = false,
     dry_run: Boolean = false,
     clean: Boolean = false,
+    list: Boolean = false,
     filter: List[String] = Nil,
     args: List[String] = Nil
   ): Process_Result = {
@@ -435,13 +437,32 @@
       "rsync --protect-args --archive --rsh=" + Bash.string("ssh -p " + port) +
         (if (verbose) " --verbose" else "") +
         (if (thorough) " --ignore-times" else " --omit-dir-times") +
+        (if (prune_empty_dirs) " --prune-empty-dirs" else "") +
         (if (dry_run) " --dry-run" else "") +
         (if (clean) " --delete-excluded" else "") +
+        (if (list) " --list-only --no-human-readable" else "") +
         filter.map(s => " --filter=" + Bash.string(s)).mkString +
         (if (args.nonEmpty) " " + Bash.strings(args) else "")
     progress.bash(script, cwd = cwd, echo = true)
   }
 
+  def rsync_dir(target: String): String = {
+    if (target.endsWith(":.") || target.endsWith("/.")) target
+    else if (target.endsWith(":") || target.endsWith("/")) target + "."
+    else target + "/."
+  }
+
+  def rsync_init(target: String,
+    port: Int = SSH.default_port,
+    contents: List[File.Content] = Nil
+  ): Unit =
+    with_tmp_dir("sync") { tmp_dir =>
+      val init_dir = make_directory(tmp_dir + Path.explode("init"))
+      contents.foreach(_.write(init_dir))
+      rsync(port = port, thorough = true,
+        args = List(File.bash_path(init_dir) + "/.", target)).check
+    }
+
   def make_patch(base_dir: Path, src: Path, dst: Path, diff_options: String = ""): String = {
     with_tmp_file("patch") { patch =>
       Isabelle_System.bash(