1

I have local history which contain commits from remote server (notice = near commit hash)

= 66346e97 (HEAD -> dev) Implement bank_statement view form
= add8964f Typo fix
< c632ef4e Store extra info while generating billed usage
< 73386b71 Send exception to e.konkov@tucha.ua
...
< a6657b24 (xtucha/dev) Code comment
< b5c7c485 Ignore invoices with same docn issued to different provider
| = 3a361741 (xxxxxx/dev) Implement bank_statement view form
| = 0ff85c07 Typo fix
|/  
o f0df4c90 Store info about where invoices are sended into databases and report

Does git have an option that allow me to push and rewrite history safely? enter image description here

Or the only option I have is rebase my commits to xxxxxx/dev upstream?

$git pull -v --rebase

and get next history:

< a65ed362 (HEAD -> dev) Store extra info while generating billed usage
< cdad7b17 Send exception to e.konkov@tucha.ua
...
< ae0ec2d9 Code comment
< 20503ea7 Ignore invoices with same docn issued to different provider
o 3a361741 (xxxxxx/dev) Implement bank_statement view form

enter image description here

Eugen Konkov
  • 22,193
  • 17
  • 108
  • 158

3 Answers3

1

Does git have an option that allow me to push and rewrite history safely?

Not exactly, no. What Git has is git push --force-with-lease.

Remember that Git finds commits by branch names (or other names) initially, but what really matters are the hash IDs. A branch name simply stores one hash ID:

66346e97 (HEAD -> dev) Implement bank_statement view form

Here, the name HEAD contained the name dev, and the name dev contained the hash ID 66346e97 (well, the full 40-character version; Git shows an abbreviation, such as the first 8 characters, to help us poor humans deal with the ugliness of hash IDs).

Commit 66346e97 itself contains the previous commit's hash ID, add8964f. So having used the name dev to find 66346e97, Git then used 66346e97 to find add8964f. Commit add8964f then allowed Git to find c632ef4e.

The general scheme here is that every commit contains the raw hash ID of its immediate predecessor. This is part of the commit's metadata (vs the data, which holds a snapshot of all of your files). If every commit points back to its parent, then we only need to find the hash ID of the last commit, to find every commit, as long as they are in nice neat chains:

... <-F <-G <-H

Here the commit with hash H is the last, so it points back to the commit with hash G, which points back to the commit with hash F, and so on. The name master might contain the actual hash ID of commit H, which we can draw this way:

...--F--G--H   <-- master

If there are additional branches, they might have commits that eventually point back to H itself:

...--F--G--H   <-- master
            \
             I--J   <-- dev

or they might have commits that point back to, say, F, or both:

       K--L   <-- rc1
      /
...--F--G--H   <-- master
            \
             I--J   <-- dev

In every case, the name points to the last commit, from which we work backwards. When we pick master, the last commit is H. When we pick dev, the last commit is J. When we pick rc1 (release candidate), the last commit is L. In every case, that last commit points backwards to some earlier commit.

When we do a "history rewrite" operation, such as rebasing commits K-L to come after commit H using:

git checkout rc1; git rebase master

we get new commits with new and different hash IDs:

             K'-L'  <-- rc1
            /
...--F--G--H   <-- master
            \
             I--J   <-- dev

The original two commits did not disappear! They are still in the repository; they just don't have a branch name for them any more:

         K--L   ???
        /
       /     K'-L'  <-- rc1
      /     /
...--F--G--H   <-- master
            \
             I--J   <-- dev

But if we had done a git push origin rc1 earlier, commits K and L—with whatever those hash IDs really are—now exist in another Git repository, over at whatever URL we have in our name origin. Our own Git remember this, by copying their name rc1 to our name origin/rc1. So this last graph looks like this:

         K--L   <-- origin/rc1
        /
       /     K'-L'  <-- rc1
      /     /
...--F--G--H   <-- master
            \
             I--J   <-- dev

Their Git still has their rc1 pointing to commit L. They do not have commits K'-L' at all yet: we only just now made them, using git rebase.

When using various viewers—including the one you're using—we can have our Git mark commits K' and L' as "equal to" commits K and L due to their cherry-picked-ness, which resulted from our running git rebase just now. So that is what you are seeing in your git log and other viewers.

If we would now like to call up the other Git over at origin, we can do any one of three things:

  • Send it commits K'-L' if needed (it will be needed), then ask it politely to set its branch name rc1 to point to commit L'. But if their rc1 still points to commit L, they will refuse, telling us that if they do that, they'll lose the commits K-L, possibly forever. Of course that's exactly what we want them to do, but what if, while we're thinking about this, someone else has added a new commit to that chain? Well, it will still refuse, in this case.

  • Or, we can send it commits K'-L' if needed (it will be), then command it to set its branch name rc1 to point to commit L'. They'll probably obey. If their rc1 pointed to L before, that did what we wanted. But if someone else had added a new commit, we just lost that new commit: not what we wanted.

  • Fortunately there is the --force-with-lease option, which will do exactly what we need.

If we use git push --force-with-lease, our Git will:

  1. Call up their Git using the URL in the name origin
  2. Offer them commit L', which ends up sending commits K' and L' because they don't have them yet;
  3. Send them the most complicated command yet:

    • We think your name rc1 contains hash ID _____ (fill in the blank from our origin/rc1, with the hash ID of commit L).
    • If that's right, we command you to set your name rc1 to point to commit _____ (fill in the blank with the hash ID of L'). If not, reject this command.
    • In any case, tell us what happened.

When we get a response, we can immediately tell whether they made their rc1 point to commit L'. If they did, all is good. If they didn't, we get a hint about why: did their rc1 point to some other commit, or are they just mad at us and refuse to update their rc1 no matter what? (We get the latter if they're on GitHub and they have set rc1 to be "protected". If we're the admin, we can override the protected state, of course.)

If their rc1 doesn't point to L any more, we should run git fetch so that we update our own origin/rc1. Then we can figure out what to do: do we rebase again? How do we want to proceed? Once we've figured it out and done it, we can do another git push --force-with-lease. If their rc1 hasn't moved, this one will work. If their rc1 has moved while were figuring stuff out, we fetch again and start over—and the cycle repeats until we can get our work done faster than anyone else can change their rc1 on us.

So:

Or the only option I have is rebase my commits to xxxxxx/dev upstream?

You should decide which commits you'd like them to have, and which commits you'd like them to throw away.

Note that if you do convince that Git to change things ... well, let's assume for the moment that that Git is on GitHub, just for concreteness. If you do convince the Git on GitHub to change their branch names so that they lose some commits, you may have just made problems for some second user with a third Git repository. Perhaps that second user is depending on commits K-L (0ff85c07 and 3a361741). That second user can easily accidentally re-introduce those original commits, e.g., by using git merge.

So any time you're going to use git push --force, with or without -with-lease, you need to be sure that all users of that other Git repository have agreed that the other Git repository's branch names get moved around like this. Having gotten an agreement from all the other users in advance, they will now know what to do when this happens.

If you are the only user of the other Git repository, you need only agree with yourself. If you don't agree with yourself, you should probably sit down with yourself and talk it over.

torek
  • 448,244
  • 59
  • 642
  • 775
0

You can try to git checkout [your commit (example:0ff85c07)] and then git checkout -b [new branch name]

And then you can continue rewrite and push from that new branch.

rzr
  • 23
  • 6
  • You do not answer what happened to `3a361741` and `f0df4c90..c632ef4e` commits?? New branch will not contain them – Eugen Konkov May 23 '20 at 10:30
  • You can read this answere. I hope it can help you https://stackoverflow.com/a/34501346/9749198 For another way, i can give my additional answers to track the issue by each commit. Step: 1. Checkout to your hash commit (example: 3a361741) 2. Now your current branch is 3a361741. From this branch, create new branch (example: fix-branch) 3. Now your current branch is fix-branch. You can continue your task (to rewrite or fixing) from this branch. And than commit and push changes to a remote repository. – rzr May 23 '20 at 14:52
  • Question is not about how to create new branch. – Eugen Konkov May 23 '20 at 16:42
0

The actions you took (git pull --rebase) was the correct one, I think the end result contains all that you want.

You can confirm that the final content of your new head (a65ed362) is the same as the content of your previous head (66346e97) :

git diff a65ed362 66346e97

When running rebase, git will detect if some commits are already present on the target branch.

In your diagram, the two commits marked with = on your local branch match the two commits on xxxxxx/dev -- in your case : they also have the same messages.

When rebasing your branch, git will not replay these commits.

The other commits will not have the same hash (for starters : they won't have the same history), but they will bring the same changes as the original commits.

LeGEC
  • 46,477
  • 5
  • 57
  • 104