+# hg-sync - Mercurial sync extension
+# Copyright (C) 2009 Oben Sonne <email@example.com>
+# This file is part of hg-sync.
+# hg-sync is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# hg-sync is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+# You should have received a copy of the GNU General Public License
+# along with hg-sync. If not, see <http://www.gnu.org/licenses/>.
+"""commit, fetch and push at regular intervals"""
+# read the sync() function documentation for detailed information
+from datetime import datetime
+from mercurial import hg
+from mercurial import commands
+from mercurial import cmdutil
+from mercurial import util
+from hgext.fetch import fetch
+from hgext.fetch import cmdtable as fetch_ct
+ALERTER = ".hgalert" # alert tool to run on errors (rel. to repo root)
+LOGFILE = os.path.join(".hg", "autosync.log") # daemon log (rel. to repo root)
+def _cmdopts(cte, ouropts):
+ """Build options dictionary for a command.
+ Option names are read from the given command table entry. Option values are
+ those set in `ouropts` or the defaults set in the command table entry.
+ key = optdesc.replace("-", "_")
+ cmdopts[key] = ouropts.get(key, optdesc)
+def _cycle(ui, root, commitopts, fetchopts, pushopts):
+ """Run a single 'commit, fetch, push' cycle"""
+ repo = hg.repository(ui, path=root)
+ ui.status("sync: commit working copy changes\n")
+ commands.commit(ui, repo, **commitopts)
+ ui.status("sync: fetch changes from other repository\n")
+ fetch(ui, repo, **fetchopts)
+ ui.status("sync: push local changes to other repository\n")
+ commands.push(ui, repo, **pushopts)
+def _sync(ui, repo, other, **opts):
+ """Synchronize once or continuously, depending on `opts`."""
+ if opts["interval"] < 1:
+ raise util.Abort("interval must be a positive number")
+ raise util.Abort("initial repository, first change should get pulled "
+ "or committed manually")
+ # set up options for sub-commands
+ commitopts = _cmdopts(commands.table["^commit|ci"], opts)
+ commitopts["message"] = "Automated commit"
+ fetchopts = _cmdopts(fetch_ct["fetch"], opts)
+ fetchopts["message"] = "Automated merge"
+ fetchopts["switch_parent"] = True
+ fetchopts["source"] = other
+ pushopts = _cmdopts(commands.table["^push"], opts)
+ pushopts["dest"] = other
+ # force non-interactive merge (unless set explicitly in repo-hgrc)
+ os.environ["HGMERGE"] = "internal:merge"
+ if repo.ui.config("ui", "merge"):
+ source = repo.ui.configsource("ui", "merge").split(":")
+ if source == os.path.join(repo.root, ".hg", "hgrc"):
+ os.environ["HGMERGE"] = repo.ui.config("ui", "merge")
+ # run one synchronization cycle only ?
+ _cycle(ui, repo.root, commitopts, fetchopts, pushopts)
+ alerter = opts["alerter"]
+ elif os.path.exists(os.path.join(repo.root, ALERTER)):
+ alerter = os.path.join(repo.root, ALERTER)
+ alerter = repo.ui.config("autosync", "alerter")
+ # loop synchronization cycles !
+ ts = datetime.strftime(datetime.now(), "%x %X")
+ ui.write("%s\n" % (" %s " % ts).center(79, "-"))
+ _cycle(ui, repo.root, commitopts, fetchopts, pushopts)
+ ui.warn("error: %s\n" % e)
+ ui.warn("sync: an error occurred, will retry at next interval\n")
+ if alerter and os.path.exists(alerter):
+ subprocess.call([alerter, repo.root, e])
+ ui.warn("sync: failed to run %s (%s)" % (alerter, e))
+def autosync(ui, repo, other="default", **opts):
+ """commit, fetch and push at regular intervals
+ Commit changes in the working copy, fetch (pull, merge and commit) changes
+ from the other repository and push local changes back to the other
+ repository - either continuously (default) or once only.
+ The idea of this command is to use Mercurial as a back-end to synchronize
+ a set of files across different machines. Think of configuration files or
+ to-do lists as examples for things to synchronize. On a higher level one
+ can say this command synchronizes not only repositories but also working
+ directories. A central repository (usually without a working copy) must be
+ used as synchronization hub:
+ repo1 <--sync--> hub <--sync--> repo2
+ Running this command in repo1 and repo2 ensures the working copies (!) of
+ both repositories stay in sync (as long as they are no conflicting
+ Errors and merge conflicts which cannot be resolved automatically are
+ highlighted in the output. Additionally an alerter tool can be specified
+ to run on errors and conflicts. This tool can be set (1) by using option
+ --alerter, (2) by placing it in the repository root as a file called
+ `.hgalert` or (3) in an HGRC file using options `alerter` in section
+ `autosync` (locations are evaluated in that order). The alerter is supposed
+ to notify errors to a human. The repository path is given as first
+ argument, an error message as the second one. Independent of this, on erros
+ and conflicts the command keeps running and retries after the next
+ interval, hoping things get fixed externally.
+ When running in daemon mode, any output gets logged into the file
+ `autosync.log` within the repository's `.hg` directory (use --daemon-log
+ to set a different file).
+ This command denies to run in a virgin repository as this may unrelate
+ repositories which were supposed to get synchronized. Before running
+ autosync, pull or commit something first manually.
+ runfn = lambda: _sync(ui, repo, other, **opts)
+ elif opts["daemon_log"] == LOGFILE:
+ logfile = os.path.join(repo.root, LOGFILE)
+ logfile = opts["daemon_log"]
+ cmdutil.service(opts, runfn=runfn, logfile=logfile)
+ [("A", "addremove", False,
+ "automatically synchronize new/missing files"),
+ ("i", "interval", 600, "synchronization interval in seconds"),
+ ("o", "once", False, "synchronize once only, don't loop"),
+ ("", "alerter", "", "program to run to alert errors"),
+ ("D", "daemon", False, "run in background"),
+ ("", "daemon-log", LOGFILE, "log file for daemon mode"),
+ ("", "daemon-pipefds", "", "used internally by daemon mode"),
+ ("", "pid-file", "", "name of file to write process ID to"),
+ ] + commands.commitopts2 + commands.remoteopts,
+ "[-A] [-i] [-D] ... [OTHER] (sync continuously)\n"
+ "hg autosync [-A] -o ... [OTHER] (sync once)")