Checking a git repo for upstream changes

03 Jul 2017

You may be aware that Bootstrap (the CSS/JS framework) is inching toward the first beta release of version 4, which has been in development since, oh, about two years ago. I like to keep track of its progress, since I use the tip version of v4 for my own work. Yeah, I know, the bleeding edge and all, but it’s so much better in my opinion that it’s worth it.

Anyway, I frequently want to see what has changed in between the times I remember to pull the new changes, and it gets tedious copying the SHA hashes to compare things, so I thought, why not write a little bash script to check all these things for me and tag the repo each time I pull changes, so I know which versions I’ve used?

NOTE: don’t do this on any repo that you’re going to push changes back to, unless you never ever push --tags. Your collaborators will almost certainly get very cranky with you about all your extraneous tags.

I’m got pretty decent git-fu, but I wasn’t expert enough to know how to ask git about the state of the remote repo without actually pulling the changes. Fortunately, Stack Overflow came to the rescue once again, and Neil Mayhew had the info I needed.

From there, it was just a matter of programming, as they say… and the result is the following script.

# Define checkrepo function. Arguments arrive as $1, $2, $3, etc.
checkrepo () {
    # Move to the repo's directory, pushing our previous location
    # onto a stack so we can go back there when we're done. The
    # argument is quoted in case some directory in the path has
    # spaces in it (increasingly common these days, unfortunately.)
    pushd "$1"

    # Define local variables (private to this function, won't
    # interfere with caller or other code
    local REMOTE_NAME="$2"
    local BRANCH_NAME="$3"

    # Get the SHA hash of the current tip of **our copy** of the remote
    # branch.  This is not the checked-out branch, but the one
    # that gets updated when you run `git fetch`.
    local us="$(git rev-list --max-count=1 ${REMOTE_NAME}/${BRANCH_NAME})"

    # Get the SHA hash of the actual remote branch (on the remote
    # server.)  This is the part I had to google to figure out.
    # Thanks, Neil! The `cut` command just separates the hash ("field 1")
    # from the rest of the text on the line.  Hint: the `cut` command
    # uses tab characters for delimiting fields by default; if you need
    # to delimit by spaces, add `-d' '` to your `cut` command.
    local them="$(git ls-remote ${REMOTE_NAME} -h refs/heads/${BRANCH_NAME} | cut -f1)"
    
    # We don't need to be in the git repo's directory anymore, so
    # pop the old location off the directory stack and go there.
    popd

    # If the two SHA hashes are the same, then no changes have been
    # pushed since we last pulled.  So, return 0 for "all is well".
    if [ $us == $them ]; then
        return 0
    else
        # There are changes, return 1 as a signal to do something.
        return 1
    fi
}

# Export the function so that other scripts
# outside our .bash_profile can use it too.
export checkrepo

# Define the function specific to our local bootstrap repo.
bboot () {
    # Set the necessary parameters as local variables.
    local REMOTE_NAME=upstream
    local BRANCH_NAME=v4-dev
    local REPO_PATH=$HOME/dev/bootstrap

    # Run our checkrepo function from above, passing the params.
    checkrepo ${REPO_PATH} ${REMOTE_NAME} ${BRANCH_NAME}

    # If the function returned 0, nothing has changed.
    if [ $? -eq 0 ]; then
        echo No changes, not pulling or tagging.
    else
        # Uh oh, there are changes.  Change to repo dir.
        pushd $REPO_PATH

        # Fetch the changes into our local copy of the remote branch
        # and then merge into the checked-out tree.
        git pull

        # Check to see if everything went well.
        if [ $? -eq 0 ]; then
            # All is well, get the current date as a formatted string
            d=$(date +%Y%m%d-%H%M%S)

            # Tag the tip commit of our local repo with the formatted
            # string so we know which commit we've pulled, for posterity
            git tag pulled-$d
        else
            # Oops, there were errors.  Bail and yell for help.
            echo There were errors during the pull... not tagging.
        fi
        
        # All done, go back to whereever our CWD was when we started.
        popd
    fi
}

# Export bboot so we can use it at the normal command line
export bboot

I separated the repo-checking code into its own function, since it might be handy for checking more than just the bootstrap repo. The bboot function also thereby serves as an example of how to call the checkrepo utility.

I use this script as part of my shell initialization, using a technique inspired by the Unixy way of writing system startup scripts. My .bash_profile script doesn’t have a whole lot in it; all the various utilities are put into individual script files, which are then placed into the $HOME/rc directory. My .bash_profile then has the following bit that sources all the .sh files in that directory:

for script in $HOME/rc/*.sh; do
    source ${script}
done

Note that I don’t execute the file; that is, I don’t call it directly as if it were a built-in command, which runs it in its own subshell. Using source means that the file is “included” (in the C meaning of that term,) just executed inline, as if it were typed into the file that’s sourcing it. This means that all the previously defined variables, functions, etc. are available to the code in that file, and the whole lot ends up as if it were in one giant .bash_profile.

Indeed, I used to have all of it in the one file, but it got very unwieldy, especially when I wanted to copy one part out to use somewhere else. Now I can just copy the one small file and be done with it.

I might separate out some things into platform-specific directories, so that I can have my whole system available unchanged on both Mac and Linux machines. But that’s for another post.