Skip to contentSkip to conversation

support/download: make the git backend even more robust

PATCH Superseded Yann E. MORIN 2 participants 10 messages Apr 22, 2018 – Apr 26, 2018

v3 of 5 Superseded Latest is v5 →

lore
Share & tools
lore
#0 lore raw

v3 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 v2 -> v3:
- do not trash the cache if the cset is missing (Thomas)

Regards,
Yann E. MORIN.

The following changes since commit 8469054fff6142d0f7684141feff6eec22aa3d57

support/config-fragments/autobuild: update ARC pre-built toolchain (2018-04-22 12:28:41 +0200)

are available in the git repository at:

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

for you to fetch changes up to 6e189d3bdfe5242fb9c3244593b7872ffb4de6e7

download/git: always do full-clone (2018-04-22 21:12:22 +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 | 120 ++++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 94 insertions(+), 26 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 v3 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 acces 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 v3 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 v3 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 usefull 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 recloning from scratch would not help, so no need to trash a good cache.

Cc Thomas Petazzoni <[email protected]>
Cc Maxime Hadjinlian <[email protected]>
Cc Ricardo Martincoski <[email protected]>
Cc Arnout Vandecappelle <[email protected]>
---
 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 v3 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 | 35 +++++++++++++++++++++++++++++++++--
 1 file changed, 33 insertions(+), 2 deletions(-)
support/download/git +33 -2
index bd37a0a8d9..921a35998b 100755
@@ -16,6 +16,33 @@ set -e
 # Environment:
 #   GIT      : the git command to call
 
+# Save out 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 uncomitted 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 v3 5/7 download/git: ensure we checkout to a clean state

In reply to Yann E. MORIN

Force the checkout to ignore 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.

Cc Maxime Hadjinlian <[email protected]>
Cc Ricardo Martincoski <[email protected]>
Cc Thomas Petazzoni <[email protected]>
Cc Arnout Vandecappelle <[email protected]>
---
 support/download/git | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
support/download/git +1 -1
index 921a35998b..e941a716ba 100755
@@ -154,7 +154,7 @@ fi
 
 # Checkout the required changeset, so that we can update the required
 # submodules.
-_git checkout -q "'${cset}'"
+_git checkout -f -q "'${cset}'"
 
 # 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 v3 5/7 download/git: ensure we checkout to a clean state

Hello,

Quoted 1 line · Yann E. MORIN

Force the checkout to ignore any local changes. This allows recovering

Nit: actually it does not 'ignore' local changes. It overwrites them. Right? The help says "to throw away local changes".

Quoted 2 lines

from a previous partial checkout (e.g. killed by the user, or by a CI job...)

This patch covers *some* cases of partial checkout, but not all. A partial checkout also can lead to untracked files.

An easy way to check this is to run on a linux tree:
git checkout -f v4.14
git checkout -f v4.4 # ctrl+c when the checkout is around 50% git checkout -f v4.5
git status

Without the next patch, that brings 'git clean', when previously there is an untracked file in the worktree, all git commands succeed, but that file is included in the tarball and the hash mismatches.

Can we bring 'git clean' to this patch?
Or do we adjust the commit title/message?

Quoted 3 lines

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

Regards,
Ricardo

#7 lore raw

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

Ricardo, All,

On 2018-04-26 00:01 -0300, Ricardo Martincoski spake thusly:

Quoted 6 lines · Yann E. MORIN
Quoted 2 lines

Force the checkout to ignore any local changes. This allows recovering

Nit: actually it does not 'ignore' local changes. It overwrites them. Right? The help says "to throw away local changes".

Yeah, right.

Quoted 9 lines
Quoted 2 lines

from a previous partial checkout (e.g. killed by the user, or by a CI job...)

This patch covers *some* cases of partial checkout, but not all. A partial checkout also can lead to untracked files.

An easy way to check this is to run on a linux tree:
git checkout -f v4.14
git checkout -f v4.4 # ctrl+c when the checkout is around 50%

Sorry, that was too fast here, I did not have the opportunity to hit Ctrl-C. Muhahaha! ;-]

Quoted 2 lines

git checkout -f v4.5
git status

But yeah, I reproduced with commits much further away from each others...

Quoted 6 lines

Without the next patch, that brings 'git clean', when previously there is an untracked file in the worktree, all git commands succeed, but that file is included in the tarball and the hash mismatches.

Can we bring 'git clean' to this patch?
Or do we adjust the commit title/message?

I'll adapt and bring git-clean here.

Thanks! :-)

Regards,
Yann E. MORIN.

Quoted 5 lines
Quoted 2 lines

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

Regards,
Ricardo

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.  |
'------------------------------^-------^------------------^--------------------'
#8 lore raw

PATCH v3 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/sub/ 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 use a 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 | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)
support/download/git +30 -0
index e941a716ba..9929acbc3f 100755
@@ -152,10 +152,40 @@ 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 submodule, effectively caching the
+# submodules.
+#
+# For older versions, 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 (that were parrents of submodules
+# removed above).
+_git clean -ffdx
+
 # Get date of commit to generate a reproducible archive.
 # %cD is RFC2822, so it's fully qualified, with TZ and all.
 date="$( _git log -1 --pretty=format:%cD )"
Signature · 1 line
2.14.1
#9 lore raw

PATCH v3 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 9929acbc3f..2d62a4249e 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