muffel
muffel

Reputation: 7390

get the git commit hash of the last commit which affected files not listed in a .gitignore-like file

I am using a CI/CD system to automate the building of Docker Images from a git repository. The Image Tag of the image corresponds to the short (i.e. 8-characters) hash of the corresponding git commit, e.g. myimage:123456ab.

The repository contains source code that gets packaged in the Docker Image and stuff like documentation and deployment configuration that is excluded using a .dockerignore file (similar to .gitignore).

While the process works in general, it leads to rebuilding and redeploying Docker Images that are absolute identical, because the only changes were made to files that did not become part of the Image (e.g. the repositories README).

Using only the shell (bash in this case), git and standard *nix tools, is there a way to get the short hash of the latest commit that changed a file which is not ignored by the .dockerignore file? This should as well cover removing a non-ignored file.

Upvotes: 1

Views: 476

Answers (2)

Léa Gris
Léa Gris

Reputation: 19675

#!/usr/bin/env bash

declare -a ign_table=()

# Populates ign_table with patterns from .dockerignore
while IFS= read -r line || [[ ${line} ]]; do
  ign_table+=("${line}")
done < <(sed '/^#/d;/^$/d' .dockerignore)

is_docker_ignored() {

  locale -i ignore=1 # false, default not ignored

  for ign_patt in "${ign_table[@]}"; do

    # If pattern starts with ! it is an exception rule
    # when filename match !pattern, do not ignore it
    # shellcheck disable=SC2053 # $ign_patt must not use quotes to match wildcards
    if [[ ${ign_patt} =~ ^\!(.*) ]] && [[ ${1} == ${BASH_REMATCH[1]} ]]; then
      return 1 # false: no need to check further patterns, file not ignored
    fi

    # Normal exclusion pattern, if file match,
    # shellcheck disable=SC2053 # $ign_patt must not use quotes to match wildcards
    if [[ ${1} == $ign_patt ]]; then
      ignore=0 # true: it match an ignore pattern, file may not be ignored if it later matches an exception pattern
    fi
  done

  return "${ignore}"
}

while IFS= read -r file
do
  is_docker_ignored "${file}" && continue # File is in .dockerignore
  commit_hash="$(git rev-list --all -1 "${file}")"
  printf '%s\n' "${commit_hash:0:8}"
done < <(git ls-files)

Upvotes: 1

Edward Minnix
Edward Minnix

Reputation: 2947

You can do this through a combination of git log and git show.

The following script will go backwards through the revision history and find the first commit to have a change that would not be ignored by .dockerignore

for commit in $(git log --pretty=%H)
do
  # Get the changed file names for the commit. 
  # Use `sed 1d` to remove the first line, which is the commit description
  files=$(git show $commit --oneline --name-only | sed 1d)
  if docker-check-ignore $files
  then
     echo $commit
     exit 0
  fi
done
exit 1

And then you could define docker-check-ignore as a script like the following:

#!/bin/sh
DIR=$(mktemp -d)
pushd $DIR
# Set up a temporary git repository so we can use 
# git check-ignore with .dockerignore
git init
popd
cp .dockerignore $DIR/.gitignore
pushd $DIR
git check-ignore $@
# Store the error code
ERROR=$? 
popd
rm -rf $DIR
exit $ERROR

I will leave reducing the number of file system operations rather than creating/removing a directory for each commit.

Upvotes: 2

Related Questions