Last Updated: September 30, 2021
·
1.626K
· ptekchand

Version Control (Push) Notifications

We switched over from a centralized version control system to a distributed system a few years ago. The biggest hurdle or point of confusion for the team seemed to be a merge.

With subversion, attempts to commit a file that had been modified (and committed to the server by another user) was met with a warning about not having the latest revision. This would be followed by an update by the developer and then a merge. This is really the same as what's done with mercurial, the only different being that a commit or update in a distributed system is a local operation. A push and a pull are the operations which go out over the network to communicate with another repository (this may be a designated central repository). With the distributed system, a developer has the benefit of being able to make smaller and more atomic commits - a Good Habit TM. Merging changes once you had multiple local commits does make the merge a little more complicated (but not that much more) - this is probably what caused some of the difficulty with the team.

To make the transition easier for the team and to make the workflow feel a little similar to subversion, we implemented "push notifications" (not the kind popularized by smart phone OSes/frameworks). This is an automated message that everyone in the team received on the internal memo/instant messaging client - every time someone (commit and) pushed some code. Depending on the volume of commits and the number of projects running in your organization, you could either broadcast a message out to everyone or only the users permitted to push code to a particular repository. Such a message allowed to pull and update (and merge) early rather than wait till a time when the merge became harder. If it doesn't break your team's flow, this may also be a great time for team members working on related areas of the code to provide quick feedback about interoperability.

[hooks]
changegroup.notifyViaIPMsg = ...

Picture

That's where the story is coming from, now for details. To crate the "push notifications", we use mercurial hooks to invoke the instant messaging program. A script is set up on the hook with the path to the repository that just received a push (a changegroup). The script then parses the configuration file of the repository (.hg/hgrc) to get the list of users that are permitted to push changes on the repository (allow_push). Each of these users then receives a message notifying them about the latest changeset. Alternately, you might choose to send a message out to everyone in your group (even those working on other projects) - this might (or not!?) be good from a motivational point of view of the whole team - more pushes going around may help with the culture of the team, perhaps a little competition on showing which project's team just made a push!

Initially we began with a simple batch file in each project's directory (.hg/pushNotify.bat), later we changed it to a common php script running on the machine which is our designated central repository - this one parses the allow_push list.

Our specific implementation is provided below as an example, we also use static IPs (or mac addresses mapped on our dhcp server), and are therefore able to throw together a quick script like this. Your team or organization may use another authentication system and messaging client and would need to do something different, but the general idea is presented below.

TestProject/.hg/hgrc

[web]
contact = Pranav Tekchand
description = Test Project is about more details
name = TestProject
allow_push = ptekchand, coderx, developery, managerz
#allow_push = *

[hooks]
# You must pass the path to the repo in quotes, especially if it contains a space. Don't add a trailing '/'
# The second parameter is the name of the repository (or project etc)
# create a IPMsgNotifyList.txt with one IP address per line for each machine you want to notify
# IPMsgNotifyList.txt not used any more common list used based on allow_push line.
changegroup.notifyViaIPMsg = d:/repos.merc/IPMsgNotify.bat "D:\repos.merc\TestProject" "TestProject"

D:\repos.merc\IPMsgNotify.bat

@echo off
REM The previous version read from a text file named IPMsgNotifyList.txt in each project's hgrc
REM This version looks up IP addresses from the usernames mentioned in the web.allow_push config
REM variable of the hgrc of the repo.. This is done from PHP.
REM See: hgPushNotify.php

REM A mercurial hook target to notify specific people of a push in a particular repository.
REM Pass the path to the repository as the first parameter to this script
REM In hgrc, under [hooks] add something like "changegroup.notifyOnIPMsg = /path/to/IPMsgNotify.bat "drive:/path/to/repo" "repoName"
REM Use back slashes as the path separator (forward slashes don't seem to work on server 2003. Though they work on XP SP3). Also, don't
REM Add a trailing '\'
REM Quick and dirty hack by (If there are any issues, get) Pranav Tekchand.

REM Configure the path to IP Messenger on this system
set PHPPATH=x:/xampp/php/php.exe
set SCRIPTPATH=x:\xampp\htdocs\learn\tools\hgPushNotify.php

set REPOPATH=%~1
set REPONAME=%~2

if not defined REPOPATH (
    echo REPOPATH not defined.
    exit /b
)
if not defined REPONAME (
    echo REPONAME not defined.
    exit /b
)

REM * X:\xampp\php\php.exe -f X:\xampp\htdocs\learn\hgPushNotify.php "D:\repos.merc\ProjectName\Platform" "ProjectName/Platform"

%PHPPATH% -f %SCRIPTPATH% "%REPOPATH%" "%REPONAME%"
REM ----------------------------------------------------------------------------------
REM your repo/.hg/hgrc should add something like this:
REM [hooks]
REM # You must pass the path to the repo in quotes, especially if it contains a space. Don't add a trailing '/'
REM # The second parameter is the name of the repository (or project etc)
REM # create a IPMsgNotifyList.txt with one IP address per line for each machine you want to notify
REM changegroup.notifyViaIPMsg = d:/repos.merc/IPMsgNotify.bat "D:/installers/IPMsg/testRepo" "test repo"

X:\xampp\htdocs\learn\tools\hgPushNotify.php

<?php
/**
 * The mercurial repositories hosted here call this PHP script to notify the people working on the project
 * about changesets being pushed.
 * Called using the hooks.changegroup hg config option
 * Parameters
 * - Path to repo.
 * - Label/Name for repo.
 *
 * Usage:
 * X:\xampp\php\php.exe -f X:\xampp\htdocs\learn\tools\hgPushNotify.php "D:\repos.merc\ProjectName\Platform" "ProjectName/Platform"
 */
set_time_limit(60);

function hgPushNotify($repoPath, $repoName) {
    $hgPath = 'D:\\Program_Files\\Mercurial\hg.exe';
    $ipMsgPath = 'd:/Installers/IPMsg/ipmsg.exe';
    // IP Addresses of users to send them IP messages.
    // TODO: Support multiple IPs for users as well.
    $baseIP = '192.168.0.';
    $userIPs = array(
        'ptekchand'=> '101',
        'coderx'=> '102',
        'developery'=> '103',
        // ...
        //''=>'',
        // Not Developers
        'managerz'=> '64',
    );

    // Resolve the repoPath's path
    $repoPathOrig = $repoPath;
    $repoPath = realpath($repoPath);
    if(false===$repoPath) {
        echo "Unable to find($repoPathOrig)";
        return false;
    }

    if(false===is_dir($repoPath)) {
        echo "Unable to switch to repo dir (".$repoPath.") - doesn't seem to be a directory.";
        return false;
    }
    // Set the repoPath to the current working directory
    chdir($repoPath);
    // repoName may be accessed from web.name
    // Parse or read web.allow_push
    $outputList = null;
    $hgOptions = 'showconfig web.allow_push';
    $cmd = $hgPath.' '.$hgOptions;
    $res = execCommand($cmd, $outputList);
    if(false===$res) {
        echo "Unable to run HG Command ($hgOptions)";
        return false;
    }
    //print_r($outputList);
    $notifyReceivers = array();
    if(is_array($outputList) && count($outputList)>0) {
        $notifyReceivers = explode(',', $outputList[0]);
    } else {
        echo "No users listed in web.allow_push for $repoName ?";
        return false;
    }

    $outputList = array();
    $hgOptions = 'log -l 1';
    $cmd = $hgPath.' '.$hgOptions;
    $res = execCommand($cmd, $outputList);
    if(false===$res) {
        echo "Unable to run HG Command ($hgOptions)";
        return false;
    }
    //print_r($outputList);
    $changesetDetails = "";
    if(is_array($outputList) && count($outputList)>0) {
        $changesetDetails = implode('. ', $outputList); // Would be cool if there was a way to send newlines.
    } else {
        echo "Unable to fetch the log details for the last changeset in $repoName ";
        $changesetDetails = "Empty commit message! Everyone descend upon the commiter! You have explaining to do, buster.";
        return false;
    }
    $logMessage = "Push Notification ($repoName)".'. '.$changesetDetails;

    foreach($notifyReceivers as $receiver) {
        $receiver = trim($receiver);
        $rIPAddress = isset($userIPs[$receiver]) ? $userIPs[$receiver] : false;
        if($rIPAddress) {
            $rIPAddress = $baseIP.$rIPAddress;
            $cmd = $ipMsgPath.' '."/MSG $rIPAddress \"$logMessage\"";
            //echo "Will execute:\n$cmd\n";
            $res = execCommand($cmd, $outputList);
        } else {
            $logMessage .= " Notification FAILED for $receiver";
        }
    }
    return true;
}

// Run a command. You should chdir to its location before calling this.
function execCommand($cmd, &$outputList) {
    if( strpos(PHP_OS, 'WIN') === 0 ) {
        //return pclose(popen($cmd, 'r'));
        return exec($cmd, $outputList);
    } else {
        echo "WARNING: Only tested on Windows so far.";
        $cmd .= ' >/dev/null 2>/dev/null & echo $! & disown';
        return exec($cmd);
    }
}

if(isset($argc)) {
    if(3 == $argc) {
        hgPushNotify($argv[1], $argv[2]);
    } else {
        echo "hgPushNotify: Not enough arguments($argc)";
    }
}
else {
    // If launched form web server to send out push notifications for specific repositories
    echo "hgPushNotify: Parameters required (repoPath, repoName)";
}