Skip to contentSkip to conversation

support/download: make the git backend even more robust

PATCH Superseded Yann E. MORIN 2 participants 10 messages May 1, 2018 – May 2, 2018

lore
Share & tools
lore
#0 lore raw

v5 cover support/download: make the git backend even more robust

Hello All!

This series makes our git backend even more robust than what we currently have. Especially, it will try to recover from a repository that was so utterly butchered that even a git-init can't salvage it.

The most significant changes are:

- ensure we can checkout from an unclean state;

- ensure we can checkout across changes in submodules setup;

- as a last-ditch recovery, trash the local cache and clone again from scratch.

Additionally, we also remove support for shallow clones, because they were in fact fundamentally broken, and did only work by chance.

Finally, we add a warning file, that the user should not use our git cache for development, neither directly in it nor as a remote or the origin for worktrees.

Thanks a lot to Ricardo, Arnout and Thomas for their inputs during the discussions that led to this series. :-)

---
Changes v4 -> v5:
- fix trapping in the _git fuction (Ricardo)
- carry reviews and tests by Ricardo
- typoes (Ricardo)

Changes v3 -> v4:
- git clean --ffdx is needed in the same patch that fircs the checkout (Ricardo)

Changes v2 -> v3:
- do not trash the cache if the cset is missing (Thomas)

Regards,
Yann E. MORIN.

The following changes since commit 5a8f887e7ab6c28b2d5a4c01266bc75943cea90c

sdl2: update license file hash (2018-05-01 09:35:41 +0200)

are available in the git repository at:

git://git.buildroot.org/~ymorin/git/buildroot.git

for you to fetch changes up to 657e448ced76f18222d9987baf7bca48d550f13b

download/git: always do full-clone (2018-05-01 10:41:02 +0200)

----------------------------------------------------------------
Yann E. MORIN (7):
download/git: add warning not to use our git cache download/git: run all git commands in the current directory download/git: quickly exit when the cset does not exist download/git: try to recover from utterly-broken repositories download/git: ensure we checkout to a clean state download/git: ensure we can checkout repos with submodule conversions download/git: always do full-clone

 support/download/git | 123 ++++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 96 insertions(+), 27 deletions(-)
Signature · 6 lines
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'
#1 lore raw

PATCH v5 1/7 download/git: add warning not to use our git cache

We really want the user not to use our git cache manually, or their changes (committed or not) may eventually get lost.

So, add a warning file, not unlike the one we put in the target/ directory, to warn the user not to use the git tree.

Ideally, we would have carried this file in support/misc/, but the git backend does not have access to it: the working directory is somewhere unknown, and TOPDIR is not exported in the environment.

So, we have to carry it in-line in the backend instead.

Cc Maxime Hadjinlian <[email protected]>
Cc Thomas Petazzoni <[email protected]>
Cc Ricardo Martincoski <[email protected]>
Cc Arnout Vandecappelle <[email protected]>
---
 support/download/git | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
support/download/git +18 -0
index bf05c595a5..3b5c8a6cfe 100755
@@ -43,6 +43,24 @@ _git() {
     eval GIT_DIR="${git_cache}/.git" ${GIT} "${@}"
 }
 
+# Create a warning file, that the user should not use the git cache.
+# It's ours. Our precious.
+cat <<-_EOF_ >"${dl_dir}/git.readme"
+	IMPORTANT NOTE!
+
+	The git tree located in this directory is for the exclusive use
+	by Buildroot, which uses it as a local cache to reduce bandwidth
+	usage.
+
+	Buildroot *will* trash any changes in that tree whenever it needs
+	to use it. Buildroot may even remove it in case it detects the
+	repository may have been damaged or corrupted.
+
+	Do *not* work in that directory; your changes will eventually get
+	lost. Do *not* even use it as a remote, or as the source for new
+	worktrees; your commits will eventually get lost.
+_EOF_
+
 # Initialise a repository in the git cache. If the repository already
 # existed, this is a noop, unless the repository was broken, in which
 # case this magically restores it to working conditions. In the latter
Signature · 1 line
2.14.1
#2 lore raw

PATCH v5 2/7 download/git: run all git commands in the current directory

In reply to Yann E. MORIN

That way, we can pushd earlier, which will help with last-ditch recovery in a followup commit.

Cc Maxime Hadjinlian <[email protected]>
Cc Thomas Petazzoni <[email protected]>
Cc Ricardo Martincoski <[email protected]>
Cc Arnout Vandecappelle <[email protected]>
---
 support/download/git | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
support/download/git +4 -4
index 3b5c8a6cfe..60d6c24f1e 100755
@@ -34,8 +34,10 @@ done
 
 shift $((OPTIND-1)) # Get rid of our options
 
-# We want to check if a cache of the git clone of this repo already exists.
+# Create and cd into the directory that will contain the local git cache
 git_cache="${dl_dir}/git"
+mkdir -p "${git_cache}"
+pushd "${git_cache}" >/dev/null
 
 # Caller needs to single-quote its arguments to prevent them from
 # being expanded a second time (in case there are spaces in them)
@@ -70,9 +72,7 @@ _EOF_
 # We can still go through the wrapper, because 'init' does not use the
 # path pointed to by GIT_DIR, but really uses the directory passed as
 # argument.
-_git init "'${git_cache}'"
-
-pushd "${git_cache}" >/dev/null
+_git init .
 
 # Ensure the repo has an origin (in case a previous run was killed).
 if ! _git remote |grep -q -E '^origin$'; then
Signature · 1 line
2.14.1
#3 lore raw

PATCH v5 3/7 download/git: quickly exit when the cset does not exist

In reply to Yann E. MORIN

Check that the given cset is indeed something we can checkout. If not, then exit early.

This will be useful when a later commit will trap any failing git command to try to recover the repository by doing a clone from scratch:
when the cset is not a commit, it does not mean the repository is broken or what, and re-cloning from scratch would not help, so no need to trash a good cache.

---
 support/download/git | 7 +++++++
 1 file changed, 7 insertions(+)
support/download/git +7 -0
index 60d6c24f1e..bd37a0a8d9 100755
@@ -114,6 +114,13 @@ if ! _git fetch origin "'${cset}:${cset}'" >/dev/null 2>&1; then
     printf "Could not fetch special ref '%s'; assuming it is not special.\n" "${cset}"
 fi
 
+# Check that the changeset does exist. If it does not, no reason to go
+# on, we can fast-track to the exit path.
+if ! _git rev-parse --quiet --verify "'${cset}^{commit}'" >/dev/null 2>&1; then
+    printf "Commit '%s' does not exist in this repository\n." "${cset}"
+    exit 1
+fi
+
 # Checkout the required changeset, so that we can update the required
 # submodules.
 _git checkout -q "'${cset}'"
Signature · 1 line
2.14.1
#4 lore raw

PATCH v5 4/7 download/git: try to recover from utterly-broken repositories

In reply to Yann E. MORIN

In some cases, the repository may be in a state we can't automatically recover from, especially since we must still support oldish git versions that do not provide the necessary commands or options thereof.

As a last-ditch recovery, delete the repository and recreate the cache from scratch.

Cc Maxime Hadjinlian <[email protected]>
Cc Thomas Petazzoni <[email protected]>
Cc Ricardo Martincoski <[email protected]>
Cc Arnout Vandecappelle <[email protected]>
---
 support/download/git | 37 ++++++++++++++++++++++++++++++++++---
 1 file changed, 34 insertions(+), 3 deletions(-)
support/download/git +34 -3
index bd37a0a8d9..39e57aac34 100755
@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
 # We want to catch any unexpected failure, and exit immediately
-set -e
+set -E
 
 # Download helper for git, to be called from the download wrapper script
 #
@@ -16,6 +16,33 @@ set -e
 # Environment:
 #   GIT      : the git command to call
 
+# Save our path and options in case we need to call ourselves again
+myname="${0}"
+declare -a OPTS=("${@}")
+
+# This function is called when an error occurs. Its job is to attempt a
+# clone from scratch (only once!) in case the git tree is borked, or in
+# case an unexpected and unsupported situation arises with submodules
+# or uncommitted stuff (e.g. if the user manually mucked around in the
+# git cache).
+_on_error() {
+    local ret=${?}
+
+    printf "Detected a corrupted git cache.\n" >&2
+    if ${BR_GIT_BACKEND_FIRST_FAULT:-false}; then
+        printf "This is the second time in a row; bailing out\n" >&2
+        exit ${ret}
+    fi
+    export BR_GIT_BACKEND_FIRST_FAULT=true
+
+    printf "Removing it and starting afresh.\n" >&2
+
+    popd >/dev/null
+    rm -rf "${git_cache}"
+
+    exec "${myname}" "${OPTS[@]}" || exit ${ret}
+}
+
 verbose=
 recurse=0
 while getopts "${BR_BACKEND_DL_GETOPTS}" OPT; do
@@ -39,6 +66,9 @@ git_cache="${dl_dir}/git"
 mkdir -p "${git_cache}"
 pushd "${git_cache}" >/dev/null
 
+# Any error now should try to recover
+trap _on_error ERR
+
 # Caller needs to single-quote its arguments to prevent them from
 # being expanded a second time (in case there are spaces in them)
 _git() {
@@ -114,8 +144,9 @@ if ! _git fetch origin "'${cset}:${cset}'" >/dev/null 2>&1; then
     printf "Could not fetch special ref '%s'; assuming it is not special.\n" "${cset}"
 fi
 
-# Check that the changeset does exist. If it does not, no reason to go
-# on, we can fast-track to the exit path.
+# Check that the changeset does exist. If it does not, re-cloning from
+# scratch won't help, so we don't want to trash the repository for a
+# missing commit. We just exit without going through the ERR trap.
 if ! _git rev-parse --quiet --verify "'${cset}^{commit}'" >/dev/null 2>&1; then
     printf "Commit '%s' does not exist in this repository\n." "${cset}"
     exit 1
Signature · 1 line
2.14.1
#5 lore raw

PATCH v5 5/7 download/git: ensure we checkout to a clean state

In reply to Yann E. MORIN

Force the checkout to ignore and throw away any local changes. This allows recovering from a previous partial checkout (e.g. killed by the user, or by a CI job...)

git checkout -f has been supported since the inception of git, so we can use it without any second thought.

Also do a forced-forced clean, to really get rid of all untracked stuff.

Cc Maxime Hadjinlian <[email protected]>
Cc Ricardo Martincoski <[email protected]>
Cc Thomas Petazzoni <[email protected]>
Cc Arnout Vandecappelle <[email protected]>
---
 support/download/git | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)
support/download/git +5 -1
index 39e57aac34..c88249ee63 100755
@@ -154,7 +154,11 @@ fi
 
 # Checkout the required changeset, so that we can update the required
 # submodules.
-_git checkout -q "'${cset}'"
+_git checkout -f -q "'${cset}'"
+
+# Get rid of now-untracked directories (in case a git operation was
+# interrupted in a previous run).
+_git clean -ffdx
 
 # Get date of commit to generate a reproducible archive.
 # %cD is RFC2822, so it's fully qualified, with TZ and all.
Signature · 1 line
2.14.1
#6 lore raw

PATCH v5 6/7 download/git: ensure we can checkout repos with submodule conversions

In reply to Yann E. MORIN

When a git tree has had sub-dir <-> sub-module conversions, or has had submodules added or removed over the course of time, checking out a changeset across those conversions/additions/removals may leave untracked files, or may fail because of a conflict of type.

So, before we checkout the new changeset, we forcibly remove the submodules. The new set of submodules, if any, will be restored later.

Ideally, we would use a native git command: git submodule deinit --all. However, that was only introduced in git 1.8.3 which, while not being recent by modern standards, is still too old for some enterprise-grade distributions (RHEL6 only has git-1.7.1).

So, instead, we just use git submodule foreach, to rm -rf the submodules directory.

Again, we would ideally use 'cd $toplevel && rm -rf $path', but $toplevel was only introduced in git 1.7.2. $path has always been there.

So, instead, we just cd back one level, and remove the basename of the directory.

Eventually, we need to get rid of now-empty and untracked directories, that were parents of a removed submodule. For example. ./foo/bar/ was a submodule, so ./foo/bar/ was removed, which left ./foo/ around.

Yet again, recent-ish git versions would have removed it during the forced checkout, but old-ish versions (e.g. 1.7.1) do not remove it with the forced checkout.

Instead we rely on the already used forced-forced clean of directories, untracked, and ignored content, to really get rid of extra stuff we are not interested in.

Cc Maxime Hadjinlian <[email protected]>
Cc Thomas Petazzoni <[email protected]>
Cc Ricardo Martincoski <[email protected]>
Cc Arnout Vandecappelle <[email protected]>
---
 support/download/git | 29 ++++++++++++++++++++++++++++-
 1 file changed, 28 insertions(+), 1 deletion(-)
support/download/git +28 -1
index c88249ee63..931694f89c 100755
@@ -152,12 +152,39 @@ if ! _git rev-parse --quiet --verify "'${cset}^{commit}'" >/dev/null 2>&1; then
     exit 1
 fi
 
+# The new cset we want to checkout might have different submodules, or
+# have sub-dirs converted to/from a submodule. So we would need to
+# deregister _current_ submodules before we checkout.
+#
+# Using "git submodule deinit --all" would remove all the files for
+# all submodules, including the corresponding .git files or directories.
+# However, it  was only introduced with git-1.8.3, which is too recent
+# for some enterprise-grade distros.
+#
+# So, we fall-back to just removing all submodules directories. We do
+# not need to be recursive, as removing a submodule will de-facto remove
+# its own submodules.
+#
+# For recent git versions, the repository for submodules is stored
+# inside the repository of the super repository, so the following will
+# only remove the working copies of submodules, effectively caching the
+# submodules.
+#
+# For older versions however, the repository is stored in the .git/ of
+# the submodule directory, so the following will effectively remove the
+# the working copy as well as the repository, which means submodules
+# will not be cached for older versions.
+#
+cmd='printf "Deregistering submodule \"%s\"\n" "${path}" && cd .. && rm -rf "${path##*/}"'
+_git submodule --quiet foreach "'${cmd}'"
+
 # Checkout the required changeset, so that we can update the required
 # submodules.
 _git checkout -f -q "'${cset}'"
 
 # Get rid of now-untracked directories (in case a git operation was
-# interrupted in a previous run).
+# interrupted in a previous run, or to get rid of empty directories
+# that were parents of submodules removed above).
 _git clean -ffdx
 
 # Get date of commit to generate a reproducible archive.
Signature · 1 line
2.14.1
#7 lore raw

PATCH v5 7/7 download/git: always do full-clone

In reply to Yann E. MORIN

We currently attempt a shallow clone, as tentative to save bandwidth and download time.

However, now that we keep the git tree as a cache, it may happen that we need to checkout an earlier commit, and that would not be present with a shallow clone.

Furthermore, the shallow fetch is already really broken, and just happens to work by chance. Consider the following actions, which are basically what happens today:

    mkdir git
    git init git
    cd git
    git remote add origin https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
    git fetch origin --depth 1 v4.17-rc1
    if ! git fetch origin v4.17-rc1:v4.17-rc1 ; then
        echo "warning"
    fi
    git checkout v4.17-rc1

The checkout succeeds just because of the git-fetch in the if-condition, which is initially there to fetch the special refs from github PRs, or gerrit reviews. That fails, but we just print a warning. If we were to ever remove support for special refs, then the checkout would fail.

The whole purpose of the git cache is to actually save bandwidth and download time, but in the long run. For one-offs, people would preferably use a wget download (e.g. with the github macro) instead of a git clone.

We switch to always doing a full clone. It is more correct, and pays off in the long run...

Cc Maxime Hadjinlian <[email protected]>
Cc Thomas Petazzoni <[email protected]>
Cc Ricardo Martincoski <[email protected]>
Cc Arnout Vandecappelle <[email protected]>
---
 support/download/git | 24 +++---------------------
 1 file changed, 3 insertions(+), 21 deletions(-)
support/download/git +3 -21
index 931694f89c..11bb52c1e1 100755
@@ -111,27 +111,9 @@ fi
 
 _git remote set-url origin "'${uri}'"
 
-# Try to fetch with limited depth, since it is faster than a full clone - but
-# that only works if the version is a ref (tag or branch). Before trying to do
-# a shallow clone we check if ${cset} is in the list provided by git ls-remote.
-# If not we fallback to a full fetch.
-#
-# Messages for the type of clone used are provided to ease debugging in
-# case of problems
-git_done=0
-if [ -n "$(_git ls-remote origin "'${cset}'" 2>&1)" ]; then
-    printf "Doing a shallow fetch\n"
-    if _git fetch "${@}" --depth 1 origin "'${cset}'"; then
-        git_done=1
-    else
-        printf "Shallow fetch failed, falling back to fetching all refs\n"
-    fi
-fi
-if [ ${git_done} -eq 0 ]; then
-    printf "Fetching all references\n"
-    _git fetch origin
-    _git fetch origin -t
-fi
+printf "Fetching all references\n"
+_git fetch origin
+_git fetch origin -t
 
 # Try to get the special refs exposed by some forges (pull-requests for
 # github, changes for gerrit...). There is no easy way to know whether
Signature · 1 line
2.14.1
#8 lore raw

v5 cover support/download: make the git backend even more robust

In reply to Yann E. MORIN

Hello,

Quoted 8 lines · Yann E. MORIN

Yann E. MORIN (7):
download/git: add warning not to use our git cache download/git: run all git commands in the current directory download/git: quickly exit when the cset does not exist download/git: try to recover from utterly-broken repositories download/git: ensure we checkout to a clean state download/git: ensure we can checkout repos with submodule conversions download/git: always do full-clone

Series applied, thanks! And thanks to Ricardo for all the testing and review!

Best regards,

Thomas

Signature · 3 lines
Thomas Petazzoni, CTO, Bootlin (formerly Free Electrons)
Embedded Linux and Kernel engineering
https://bootlin.com
#9 lore raw

v5 cover support/download: make the git backend even more robust

Thomas, All,

On 2018-05-01 21:25 +0200, Thomas Petazzoni spake thusly:

Quoted 11 lines · Yann E. MORIN
Quoted 8 lines

Yann E. MORIN (7):
download/git: add warning not to use our git cache download/git: run all git commands in the current directory download/git: quickly exit when the cset does not exist download/git: try to recover from utterly-broken repositories download/git: ensure we checkout to a clean state download/git: ensure we can checkout repos with submodule conversions download/git: always do full-clone

Series applied, thanks! And thanks to Ricardo for all the testing and review!

Indeed, thanks a *lot* to Ricardo for the interesting reviews and the extensive testing. Very much appreciated! :-)

Regards,
Yann E. MORIN.

Signature · 6 lines
.-----------------.--------------------.------------------.--------------------.
|  Yann E. MORIN  | Real-Time Embedded | /"\ ASCII RIBBON | Erics' conspiracy: |
| +33 662 376 056 | Software  Designer | \ / CAMPAIGN     |  ___               |
| +33 223 225 172 `------------.-------:  X  AGAINST      |  \e/  There is no  |
| http://ymorin.is-a-geek.org/ | _/*\_ | / \ HTML MAIL    |   v   conspiracy.  |
'------------------------------^-------^------------------^--------------------'