Strange Behaviour of Git: Mysterious Changes Cannot Be Undone

Strange behaviour of Git: mysterious changes cannot be undone

The Linux source tree has filenames which differ in case only, which causes interesting failures on systems with case-insensitive filesystems.

You need a case-sensitive filesystem in order to work with the Linux source.

(include/linux/netfilter/xt_connmark.h and include/linux/netfilter/xt_CONNMARK.h are two different files in the Git repository, but only one can exist in your checkout at a time if your filesystem is case-insensitive.)

Git shows file contents changed after clone

Check these questions:

  • Cloning a git repo, and it already has a dirty working directory... Whaaaaa?
  • Strange behaviour of Git: mysterious changes cannot be undone

This problem is caused by the OS X filesystem being case insensitive.

Cloning a git repo, and it already has a dirty working directory... Whaaaaa?

Which OS are you using? This error is caused by your filesystem not being case sensitive, like the default on Mac OS X.

How can I discard modified files?

A git reset --hard HEAD should solve the problem.

Strange behavior from git log --since

By default, the date in git log will show in the default format.

--date=default shows timestamps in the original timezone (either committer’s or author’s).

Based on Git help log

--date=local shows timestamps in user’s local timezone.

--date=default shows timestamps in the original timezone (either committer’s or author’s).

I suggest you run git log --date=local --since=<your date>. It's supposed to show all commits in your local machine's time.

Now, back to your question about --since.

If --since=<date> is without specific a time, it will use your local time at the time you run this command.

For example,

--since "20-09-2013"

when you run this command at 09AM, will act as

--since "20-09-2013 09:00:00"

so, that's why it doesn't make some commits on that day show up. (because it already passed your current time)

if you would like to search for all commits for today then use

--since "20-09-2013 00:00:00"

back to mystery story

So, now git log --since=19-09-2013. That returns

Date: Fri Sep 20 08:04:13 2013 +0200

Date: Fri Sep 20 08:03:28 2013 +0200

Date: Fri Sep 20 08:02:05 2013 +0200

Date: Thu Sep 19 09:53:10 2013 +0200

What I guess that you have run this command from 08:04:13 afterward on 20 Sep 2013. (Based on your hint that when you use since today, it shows nothing.).

How to force git to use fast-forward when applying a stash?

TL;DR

You need to git reset --hard HEAD (or anything equivalent) before applying with --index like this. All the usual caveats around hard resets apply.

Long

I linked in a comment to How do I properly git stash/pop in pre-commit hooks to get a clean working tree for tests? which shows how to do the final pop (or equivalent), and some of the caveats around this. However, the answer to the question as asked—specifically How to force git to use fast-forward when applying a stash—is that you can't, and in fact, the question doesn't even make sense: fast-forward is a different concept from stashing and unstashing.1

A Git stash is simply a set of commits (two unless you use the --all or --include-untracked option, then you get three) with a special arrangement. The commits save:

  • the index at the time of git stash (using git write-tree);
  • the work-tree contents at the time of git stash (using rather complex code);
  • and last in this list, but actually done earlier, if you did use --all or --include-untracked, the files that were untracked including ignored files (--all) or the files that were untracked excluding ignored files (--include-untracked).

Git then resets the work-tree, normally to match the HEAD commit, and if --all or --include-untracked were used, removes the files stored in the third commit as well. When you use --keep-index, though, Git resets the work-tree to match the index contents.

The reference named refs/stash is modified to point to the work-tree commit. This commit has, as its parents, the HEAD commit (parent #1), the index commit (parent #2), and if present, the untracked-files commit (parent #3). The index has as its parent the HEAD commit. The untracked-files commit has no parent (is a root commit):

...--o--o--o   <-- refs/heads/somebranch (HEAD)
|\
i-w <-- refs/stash
/
u

or more typically, the same without u.

When git stash resets to HEAD (i.e., without --keep-index), all you have to do undo what git stash did is run git stash pop --index (note: not --keep-index!). This runs git stash apply with the same options and arguments,2 and if that succeeds without merge conflicts, runs git stash drop on the same stash.

The apply can use both the index commit and the work-tree commit to recover what you were working on, but by default, it ignores the index commit. Adding --index tells Git to apply the index commit (turned into a diff against the current index contents) to the current index contents, using git apply --index. If this fails, git stash stops and does nothing. In this I would suggest one turn the stash into a new branch using git stash branch, though git stash merely suggests applying without --index.3

In any case, Git then tries to apply the work-tree commit to the current work-tree.4 If you had stashed without --keep-index, and made no changes to the current work-tree, this would always succeed: the current index and work-tree would match the HEAD commit so this would leave the current index unchanged and apply all the differences in the work-tree commit to the work-tree itself, resulting in recovery of the stashed work-tree.

The problem at this point is that you did use --keep-index, so the current work-tree matches the index that you set up, rather than matching the HEAD commit. Hence, before you apply the stash (with or without --index), you must first reset the work-tree to match the HEAD commit, i.e., git reset --hard. The index and work-tree states you want are in the stash you're about to apply, so this is safe as long as the current index and work-tree have not been modified by whatever pre-commit / pre-push code you have.

Once you have done that, a git apply --index of the stash commits will restore both the index and the work-tree (modulo that bug in the linked question!).


Footnotes

These are out of order on purpose because footnote 1 is so long.

2The argument to git stash apply defaults to refs/stash. If you give it any argument, the behavior is a little fancier: in recent versions of Git, if you give it an all-numeric argument n it examines stash@{n}, otherwise it uses whatever you gave it. It passes this string to git rev-parse to make sure that it converts to a valid hash ID, and that when suffixed with :, ^1, ^1:, ^2, and ^2:, those also convert to valid hash IDs. If the string produces a valid hash ID with both ^3 and ^3:, those are also remembered. These collectively form the w_commit, w_tree, b_commit, b_tree, i_commit, and i_tree, plus the u_commit and u_tree if they exist. See the gitrevisions documentation for how this works in more detail.

What this boils down to is that any argument you pass to git stash apply must have the form of a merge commit, with at least two parents. Git does not check whether there are additional parents beyond the prospective three, nor whether this merge commit really is a stash: it just assumes that if it has the right set of parent-age, you intend to use it as one.

3This might be sensible enough for Git neophytes who are not trying to stash the index separately and used --index on git stash apply or git stash pop without understand it. Once you do understand the index, though, it's clearly wrong: you wanted to restore the stashed index's changes relative to your current index, to your current index, not ignore them entirely! Committing your current index if appropriate, then committing your current work-tree if appropriate, and then turning the stash into a branch and committing its work-tree, gives you everything you need to build the correct final results.

4Technical details: the application uses git merge-recursive—this is what implements git merge -s recursive—with some secret environment variables to set the names on the conflict markers, if there is a conflict. The merge base is the commit that was HEAD when the stash was made, the current tree is the the result of writing the current (at un-stash time) index, and the item being merged is the work-tree commit, or more precisely, its tree. This makes use of the fact that some merges can be run with uncommitted changes. The front end git merge command prohibits merge attempts with uncommitted changes, as the results can be very messy when there's a problem.

1The fast-forward concept is also a bit more complicated than one typically sees it at first. That is, we see it when merging—see What is the difference between `git merge` and `git merge --no-ff`?—but it actually refers to updating a reference, such as a branch name. A branch name update is a fast-forward if and only if the new commit hash has the old commit hash as an ancestor, i.e., if git merge-base --is-ancester $old_hash $new_hash returns a zero exit status.

When git merge performs one of these fast-forward operations, it means that Git has changed the HEAD commit to point to the new hash, and also updated the index and work-tree as necessary. If you were to fast-forward to the work-tree commit in the stash, that would expose the weird technically-a-merge work-tree commit to the rest of Git, where it would be, at the least, very confusing.

Note that git fetch and git push also perform fast-forward operations, or with --force, allow non-fast-forward changes to branch and (for fetch) remote-tracking names. The receiver of a push normally requires a fast-forward because that means that the updated branch name contains all of the commits that it used to, plus some additional commits. A forced, non-fast-forward update discards commits from the branch (whether or not it adds new ones). The somewhat mysterious git fetch output records whether a remote-tracking name was fast-forwarded or forced in three (!) ways:

$ git fetch
remote: Counting objects: 1701, done.
remote: Compressing objects: 100% (711/711), done.
remote: Total 1701 (delta 1363), reused 1318 (delta 989)
Receiving objects: 100% (1701/1701), 975.29 KiB | 3.65 MiB/s, done.
Resolving deltas: 100% (1363/1363), completed with 284 local objects.
From [url]
3e5524907..53f9a3e15 master -> origin/master
61856ae69..ad0ab374a next -> origin/next
+ fc16284ea...4bc8c995a pu -> origin/pu (forced update)
9125ddae1..9db014fc5 todo -> origin/todo
* [new tag] v2.18.0 -> v2.18.0
* [new tag] v2.18.0-rc2 -> v2.18.0-rc2

Note the + in front of line recording the update to origin/pu, and the words (forced updated) added. That's two of the three ways. Pay attention to the dots between the two abbreviated commit hashes, though: all the other lines, which are not forced updates, show two dots, but this update shows three dots. That's because we can use git rev-list or git log with this same three-dot syntax to view the commits added and removed:

$ git log --oneline --decorate --graph --left-right fc16284ea...4bc8c995a
> 4bc8c995a (origin/pu) Merge branch 'sb/diff-color-move-more' into pu
|\
| > 76db2b132 SQUASH????? Documentation breakage emergency fix
| > f2d78d2c6 diff.c: add white space mode to move detection that allows indent changes
| > a58e68b88 diff.c: factor advance_or_nullify out of mark_color_as_move
[massive snippage]
< fc16284ea Merge branch 'mk/http-backend-content-length' into pu
|\
| < 202e4a2ff SQUASH???
| < cb6d3213e http-backend: respect CONTENT_LENGTH for receive-pack
< | 4486a82e5 Merge branch 'ag/rebase-p' into pu
< | a84cc85f3 Merge branch 'nd/completion-negation' into pu
[much more snippage]

The --left-right option, along with the three-dot syntax, tells Git to mark which "side" the commits came from. In this case the > commits are now on the pickup branch, and the < commits have been taken off it. These particular removed commits are now entirely unreferenced and will be garbage collected soon(ish).

Git push not working in visual studio 2015

On further inspection I found a difference between the GIT command line client and the builtin cliënt in visual studio. The latter uses TLS1.2 for the https session, whereas the command line uses TLS 1.0.

As it happens out private GITLAB server is situated behind a reverse proxy that has problems with TLS1.2 connections resulting in the weird behaviour (fetch & pull OK, no push).

Retract accidental checkin

NB: THIS PROBABLY WON'T WORK ON CURRENT VERSIONS OF SUBVERSION AND IS A BAD IDEA - but I've left it here for information

NB: Normally when you have checked in by mistake, you should just revert the commit - see the other answers to this question. However, if you want to know how to actually undo the effects of the commit and change the repository to be how it was before, there's some explanation below:

This isn't what you normally want, but if you really want to remove the actual committed version from the repository, then you can do a nasty rollback on the repository as follows (this assumes that $REV is set to the latest revision, which you are removing):

  • Backup your repository first as these changes may break it (and read the assumptions below)
  • Revert your local copy to the previous revision so it doesn't get confused (svn revert -r $((REV-1)))
  • In the repository, remove db/revs/$REV and db/revprops/$REV
  • In the repository, remove db/current and (for subversion 1.6 or greater) db/rep-cache.db, and run svnadmin recover .
  • (Possibly) adjust the permissions on db/rep-cache.db to prevent attempt to write a readonly database errors

This all assumes:

  • You're using a fsfs-based repository
  • Subversion release greater than 1.5.0 (otherwise you have to manually edit db/current and change the revision number rather than running svnadmin recover .)
  • No other subsequent revisions have been committed
  • You have write access to the filesystem of the repository
  • You aren't scared of somebody else trying to access it while you do the above

I've done it when a huge file was committed to a repository that I didn't want to stay in the history (and mirrors etc) forever; it's not in any way ideal or normal practice...



Related Topics



Leave a reply



Submit