0

A bit of context

I am develloping a flow which will allow people to access a common environment. The idea is to have a repository that should only be pushed in, but not a bare one as I want people to be able to read it. Let's call it COMMON
This is because I do not want people to have to clone the repository to be able to access the data.

To do it, I have the following structure:

  1. Local repository: where the ones who want to develop the flow can perform modifications, commit and push to the remote repository
  2. Remote repository: A bare repository that is also used as a gateway. Whenever a push is done to this bare repository it is repercuted to the COMMON repository
  3. COMMON repository: the public 'read' directory. Only a push can modify its history, even commit is forbidden(via the pre-commit hook)

Problematic

The COMMON repository should not have any direct modifications. However, as multiple users are sudoers, I cannot be 100% sure that someone will not directly modify it.
Or, accordingly with Murphy, I am 100% sure that someone will one day modify something locally in the COMMON repository...

To be sure that there is no conflict when someone rightfully pushes into the remote directory, and consequently in the COMMON repository, I thought about the following hooks for the COMMON:

  • pre-receive
#!/bin/bash
#Stash local modifications if there are ones to avoid conflicts
echo "## Local modifications on the Product-Line are stashed with the tag: 'local modifications @ $(date)' "
git stash push -m "local modifications @ $(date)"
exit 0
  • post-receive
#!/bin/bash
#Checks out the the branch that contains the last commit
git checkout $( git log --branches -1 --pretty=format:'%D' | sed  -e 's/.*-> //g' -e 's/.*, //g' )
#Forces synchronization with the last commit
git stash
git stash drop stash@{0}

exit 0

However, the stash commands does not seem to work as expected. The log returned when a push is done in the local repository is the following :

$> echo toto >> stash_test ; git commit -am "test" ; git push
[tbranch f6e8fbb] test
 1 file changed, 1 insertion(+)
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 48 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 351 bytes | 351.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: remote: ## Local modifications on the Product-Line are stashed with the tag: 'local modifications @ Mon Dec 23 17:44:59 CET 2019'         
remote: remote: No local changes to save        
remote: remote: Already on 'tbranch'        
remote: remote: M   stash_test        
remote: remote: Saved working directory and index state WIP on tbranch: f6e8fbb test        
remote: remote: stash@{0}: WIP on tbranch: f6e8fbb test        
remote: remote: Dropped stash@{0} (b29b76ceb2b2dfd2fcf6d0577c7200e517641328)        
remote: To ../pl
remote:    f75b5d1..f6e8fbb  tbranch -> tbranch
To /home/usr/TEMP/remote
   f75b5d1..f6e8fbb  tbranch -> tbranch

Two issues here:

  • To test the auto stashing in the COMMON repository, I modified a file. However when the pre-receive hook reaches the git stash push -m "msg", it returns No local changes to save
  • In the post-receive hook, the git stash plus git stash drop stash@{0} should emulate an update to the last commit of the current branch. In the log, it appears that the operation worked, however, if I get the status in the COMMON repository I obtain :
On branch tbranch
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   stash_test
    modified:   test

  • stash_test is the file modified by the last commit
  • test is the file locally modified to test the all procedure

Double dragon effect: 1. the git stash list is empty, where it should contain the modification of test. However the log is coherent with that as it did not stash test in the pre-receive hook
2. stash_test is not updated with the last commit... Here it is strange as the log indicates that everything was okay...

Where I am lost is that I tried to perform the steps manually and... it works as I expected.
I however am quite new in git administration so I could have missed something...

Questions

  • Does the git stash command have a fancy behavior in the hooks that does not allow me to do what I want ?
  • Is there another (maybe better) way to achieve what I want to do ?

EDIT: Issue root cause identified

When a hook is executed, it is in the .git directory of my COMMON dir. Hence, the git stash does not detect the modifications.
If I perform a git status --porcelain in the pre-receive hook I get:

?? HEAD
?? ORIG_HEAD
?? config
?? description
?? gitk.cache
?? hooks/applypatch-msg.sample
?? hooks/commit-msg.sample
?? hooks/fsmonitor-watchman.sample
?? hooks/post-receive
?? hooks/post-update.sample
?? hooks/pre-applypatch.sample
?? hooks/pre-commit
?? hooks/pre-commit.sample
?? hooks/pre-push.sample
?? hooks/pre-rebase.sample
?? hooks/pre-receive
?? hooks/pre-receive.sample
...

Which is expected as the content of the .git dir are not under version control.

In fact, even with the @knittl suggestion of using a git reset --hard does not work as it is not done in the 'work' directory of common.

The trivial workaround I thought about is to encapsulate the operations in my hooks with:

pwd # returns COMMON/.git
pushd .. > /dev/null
pwd # returns COMMON
#hook operations
popd > /dev/null

Checking withs pwd commands in the hooks I finally am in the correct directory to perform my stash/reset.
However, when doing it, the remote message returns me:

remote: remote: fatal: not a git repository: '.'        

So now I am a bit confused as I am actually at the root of my COMMON so it should be considered as a git repository...

Even if, as suggested in by @knittl and @torek, it is not the golden way to deploy something, I would like to understand why there are these limitations and if there is a way to override them.

EDIT2: Stash in pre-commit

In the pre-commit hook I perform a stash to save the local modification and abort the commit so the 'reckless sudoer' cannot commit its changes but can later cherry-pick his modifications. In this hook, the git stash performs as expected.

I guess that the issue is related with the fact that pre/post-receive hooks are server-side hooks and should normally only be used for bare repository.

Krouitch
  • 596
  • 3
  • 13
  • 4
    I'd suggest a totally different approach: Don't use Git for deployment. Don't have a Git repository on the COMMON system at all. Use a deployment tool to deploy from the bare repository (which everyone can clone, and appropriately authorized users can push-to). With no Git repo on COMMON there is no way for anyone to do any work there, hence nothing to stash. – torek Dec 23 '19 at 18:11
  • 1
    Are you pushing to the checked-out branch in your common repository? (I thought Git usually prevents this). But if that's the case, then of course your locally changed and then pushed file is "modified" in your "common" working tree. It does not match the committed version anymore (because it still contains the *old* content). Did you run `git diff` to see the actual differences after pushing? – knittl Dec 23 '19 at 18:53
  • @torek The thing is that deploying using Git allows me to use it as a watchguard. Even if git is not used, then reckless sudoers could nevertheless modify somethings. With Git, I can simply stash the changes and the 'reckless sudoer' can then cherry-pick from the stash the modification he shouldn't have done in the COMMON repository to report it in its local repository once he obtained his local repository. I will however look into other ways to easily and automatically deliver the modifications – Krouitch Dec 23 '19 at 19:00
  • @knittl I authorized the push by setting ``receive.denyCurrentBranch=ignore``. I actually did the diff and it showed what I expected, *i.e.* the changes in the commit appeared as not applied to the local file. That is why after the pushm and the checkout to the branch containing the last commit, I perform ``git stash`` and ``git stash drop stash@{0}`` to ignore these modifications and so be in line with the last commit – Krouitch Dec 23 '19 at 19:06
  • 1
    I see, why are you going via the stash? To have a backup, in case something goes wrong? But as soon as you drop the stash, you have to do detective work to find the old, deleted stashes. Why not do a `git reset --hard` and `git clean` to sync the working tree with the commit? (But, as mentioned in other comments above, I wouldn't push to a non-bare repo either. Better to push into a bare repo and have another repo pull from there or use some other, better deployment tool) – knittl Dec 23 '19 at 19:13
  • I will try with the ``reset``. The most important stash is the pre-receive one. It is meant to keep the elements that were modified by the 'reckless sudoer'. In the post-receive I only dropped the first stash on the pile which contains the differences between the current state and the last commit. There is not a direct push to the COMMON. In fact, the push is to the remote repository and as a post-receive hook, the bare repository pushes into the COMMON. It is equivalent to an ''automatic pull'' when something is changed in the remote repository. – Krouitch Dec 23 '19 at 19:32
  • You should probably use hook [push-to-checkout](https://git-scm.com/docs/githooks#_push_to_checkout) – max630 Dec 24 '19 at 11:05

1 Answers1

0

I finally found the piece of documentation that was messing with me :

Before Git invokes a hook, it changes its working directory to either $GIT_DIR in a bare repository or the root of the working tree in a non-bare repository. An exception are hooks triggered during a push (pre-receive, update, post-receive, post-update, push-to-checkout) which are always executed in $GIT_DIR.

So the GIT_DIR environment variable makes my hook run in COMMON/.git. So to override that I finally changed the current working directory to COMMON and set GIT_DIR=$(pwd -P)/.git
If GIT_DIR is not set that way, then git does not consider COMMON as a git directory (because by default GIT_DIR='.' and git seeks in the upper levels to find a .git directory)

For reference, my post-receive hook finally looks like:

#!/bin/bash

# Make sure that we are on the branch containing the last commit
git checkout -f $( git log --branches -1 --pretty=format:'%D' | sed  -e 's/.*-> //g' -e 's/.*, //g' )

# Forces synchronization with the last commit by discarding the local modifications. A bit trickerish but it works...
cd ..
export GIT_DIR=$(\pwd -P)/.git
git reset --hard

exit 0
Krouitch
  • 596
  • 3
  • 13