@@ 0,0 1,214 @@
+#!/usr/bin/python
+
+# =============================================================================
+#
+# hg-sync - Mercurial sync extension
+# Copyright (C) 2009 Oben Sonne <obensonne@googlemail.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
+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)
+ return
+
+ # 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)
+ else:
+ alerter = repo.ui.config("autosync", "alerter")
+
+ # loop synchronization cycles !
+
+ while True:
+ ts = datetime.strftime(datetime.now(), "%x %X")
+ ui.write("%s\n" % (" %s " % ts).center(79, "-"))
+ try:
+ _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):
+ try:
+ subprocess.call([alerter, repo.root, e])
+ except OSError, e:
+ ui.warn("sync: failed to run %s (%s)" % (alerter, e))
+ finally:
+ ui.flush()
+ time.sleep(opts["interval"])
+
+# =============================================================================
+# 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
+ changes).
+
+ 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)
+ else:
+ 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)")
+}
+
@@ 0,0 1,165 @@
+#!/bin/sh
+
+# enable autosync extension when using Mercurial's run-tests.py
+[ -n "$HGRCPATH" ] && cat >> $HGRCPATH <<EOF
+[extensions]
+hgext.autosync=
+EOF
+
+# -----------------------------------------------------------------------------
+# constants
+# -----------------------------------------------------------------------------
+
+SYNCIVAL=4
+
+# -----------------------------------------------------------------------------
+# utilities
+# -----------------------------------------------------------------------------
+
+wait() {
+ IVAL=$((2 * $SYNCIVAL))
+ echo "-> waiting for synchronization ($IVAL seconds)"
+ sleep $IVAL
+}
+
+# -----------------------------------------------------------------------------
+# init
+# -----------------------------------------------------------------------------
+
+rm -rf tenv
+mkdir tenv
+cd tenv
+
+echo "-> set up test repos"
+hg init hub # sync hub
+echo h > hub/f0
+hg -R hub commit -Am "Initial manual commit" -d "0 0" -u hub
+hg clone hub ra # a local clone
+hg clone hub rb # a local clone
+
+echo "-> start sync daemons"
+hg -R ra -v autosync -A -i $SYNCIVAL -D --pid-file=sd-ra.pid -d "0 0" -u ra
+sleep $(($SYNCIVAL / 2))
+hg -R rb -v autosync -A -i $SYNCIVAL -D --pid-file=sd-rb.pid -d "0 0" -u rb
+sleep $(($SYNCIVAL / 4))
+
+# -----------------------------------------------------------------------------
+# test
+# -----------------------------------------------------------------------------
+
+NREV=0 # expected number of revisions
+
+echo "-> ra only change"
+echo a > ra/f1
+wait # ra commits and pushes | rb pulls
+NREV=$((NREV+1))
+
+echo "-> rb only change"
+echo b > rb/f2
+wait # rb commits and pushes | ra pulls
+NREV=$((NREV+1))
+
+echo "-> identical changes in ra and rb"
+echo ab > ra/f3
+echo ab > rb/f3
+wait # ra commits and pushes | rb commits, pulls, merge and pushes | ra pulls
+NREV=$((NREV+3))
+
+echo "-> non-conflicting changes in ra and rb"
+echo a > ra/f4
+echo b > rb/f5
+wait # ra commits and pushes | rb commits, pulls, merge and pushes | ra pulls
+NREV=$((NREV+3))
+
+echo "-> conflicting change in ra and rb"
+echo a > ra/f6
+echo b > rb/f6
+wait # ra commits and pushes | rb commits, pulls, fails in merging
+NREV=$((NREV+1))
+
+echo "-> manually resolve merge conflict"
+rm rb/f6.orig
+echo "ab" > rb/f6
+hg -R rb resolve -a -m
+wait # rb commits and pushes | ra pulls
+NREV=$((NREV+2))
+
+echo "-> stop sync dameons"
+kill `cat sd-ra.pid` || echo "failed: sync daemon for ra crashed"
+kill `cat sd-rb.pid` || echo "failed: sync daemon for rb crashed"
+
+echo "-> configure repos to use internal:local for merge"
+for repo in ra rb ; do
+ echo "[ui]" >> $repo/.hg/hgrc
+ echo "merge = internal:local" >> $repo/.hg/hgrc
+done
+
+echo "-> start sync daemons"
+hg -R ra -v autosync -A -i $SYNCIVAL -D --pid-file=sd-ra.pid -d "0 0" -u ra
+sleep $(($SYNCIVAL / 2))
+hg -R rb -v autosync -A -i $SYNCIVAL -D --pid-file=sd-rb.pid -d "0 0" -u rb
+sleep $(($SYNCIVAL / 4))
+
+echo "-> conflicting change in ra and rb"
+echo a > ra/f7
+echo b > rb/f7
+wait # ra commits and pushes | rb commits, pulls, merges and pushes | ra pulls
+NREV=$((NREV+3))
+
+echo "-> stop sync dameons"
+kill `cat sd-ra.pid` || echo "failed: sync daemon for ra crashed"
+kill `cat sd-rb.pid` || echo "failed: sync daemon for rb crashed"
+
+echo "-> conflicting change in ra and rb"
+echo a > ra/f8
+echo b > rb/f8
+echo "-> run autosync in both repos, non-looping and non-daemon"
+echo "-------- non-looping, no-daemon mode --------" >> ra/.hg/autosync.log
+hg -R ra -v autosync -A -o -d "0 0" -u ra >> ra/.hg/autosync.log 2>&1
+echo "-------- non-looping, no-daemon mode --------" >> rb/.hg/autosync.log
+hg -R rb -v autosync -A -o -d "0 0" -u rb >> rb/.hg/autosync.log 2>&1
+# wait # ra commits and pushes | rb commits, pulls, merges and pushes
+NREV=$((NREV+3))
+
+echo "-> conflicting change in ra and rb while not completely synchronized"
+echo a > ra/f9
+echo b > rb/f9
+echo "-> run autosync in both repos, non-looping and non-daemon"
+hg -R ra -v autosync -A -o -d "0 0" -u ra >> ra/.hg/autosync.log 2>&1
+hg -R rb -v autosync -A -o -d "0 0" -u rb >> rb/.hg/autosync.log 2>&1
+# wait # ra commits, pulls, merges and pushes | rb commits, pulls, merges and pushes
+hg -R ra -v autosync -A -o -d "0 0" -u ra >> ra/.hg/autosync.log 2>&1
+# wait # ra pulls
+NREV=$((NREV+4))
+
+# -----------------------------------------------------------------------------
+# check result
+# -----------------------------------------------------------------------------
+
+echo "-> check if repos equal"
+
+ID_RA=`hg -R ra id -i --debug -r tip`
+ID_RB=`hg -R rb id -i --debug -r tip`
+
+echo "-> id ra: $ID_RA"
+echo "-> id rb: $ID_RB"
+
+[ "$ID_RA" = "$ID_RB" ] || echo "failed: repo tip ids differ"
+
+NREV_RA=`hg -R ra id -n -r tip`
+NREV_RB=`hg -R rb id -n -r tip`
+
+echo "-> nrev expected: $NREV"
+echo "-> nrev ra : $NREV_RA"
+echo "-> nrev rb : $NREV_RB"
+
+[ "$NREV" = "$NREV_RA" ] || echo "failed: ra has wrong number of revisions"
+[ "$NREV" = "$NREV_RB" ] || echo "failed: rb has wrong number of revisions"
+
+# -----------------------------------------------------------------------------
+# clean up
+# -----------------------------------------------------------------------------
+
+#kill `cat sd-ra.pid` || echo "failed: sync daemon for ra crashed"
+#kill `cat sd-rb.pid` || echo "failed: sync daemon for rb crashed"
+