hg-autosync /


# =============================================================================
#    hg-sync - Mercurial sync extension
#    Copyright (C) 2009 Oben Sonne <>
#    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
#    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 <>.
# =============================================================================

"""commit, fetch and push at regular intervals"""

# read the sync() function documentation for detailed information

from datetime import datetime
import os.path
import subprocess
import time

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

# =============================================================================
# constants
# =============================================================================

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)

# =============================================================================
# utility functions
# =============================================================================

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.
    cmdopts = {}
    for optdesc in cte[1]:
        key = optdesc[1].replace("-", "_")
        cmdopts[key] = ouropts.get(key, optdesc[2])
    return cmdopts

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`."""
    # check options
    if opts["interval"] < 1:
        raise util.Abort("interval must be a positive number")
    if not repo.changelog:
        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(":")[0]
        if source == os.path.join(repo.root, ".hg", "hgrc"):
            os.environ["HGMERGE"] = repo.ui.config("ui", "merge")
    # run one synchronization cycle only ?
    if opts["once"]:
        _cycle(ui, repo.root, commitopts, fetchopts, pushopts)
    # detect alerter tool
    if opts["alerter"]:
        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 !
    while True:
        ts = datetime.strftime(, "%x %X")
        ui.write("%s\n" % (" %s " % ts).center(79, "-"))
            _cycle(ui, repo.root, commitopts, fetchopts, pushopts)
        except util.Abort, e:
            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):
          [alerter, repo.root, e])
                except OSError, e:
                    ui.warn("sync: failed to run %s (%s)" % (alerter, e))

# =============================================================================
# extension interface
# =============================================================================

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)
    if not opts["daemon"]:
        logfile = None
    elif opts["daemon_log"] == LOGFILE:
        logfile = os.path.join(repo.root, LOGFILE)
        logfile = opts["daemon_log"]
    cmdutil.service(opts, runfn=runfn, logfile=logfile)
# =============================================================================
# command table
# =============================================================================

cmdtable = {
    "autosync": (autosync,
        [("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"),
         # daemon options
         ("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)")