'What will happen if I push changes from one branch to another?

I have two branches in my project.

main and test_deploy

It's incorrect to push changes without any tests to the main branch, but never mind.

I use the main branch to make some changes locally and the test_deploy where I have other changes in settings.py (for example DEBUG=False, use a cloud for storing my model images and so on) and files for deployment(Procfile, runtime.txt and so on).

For example, I'm going to add a new app and push it to the main branch and to the test_deploy branch too(to get the newest version of my project)

I have a question. What will happen if I commit these changes to the main branch, push it to this branch AND also push it to the test_deploy branch? Will I have conflicts that I'll have to fix manually? Or I have firstly pull all files from the test_deploy branch and then push it?(I don't wanna pull files from the test_deploy branch. That's why I asked this question)

Summary

So, generally, I wanna push changes from the main branch to test_deploy, but without pulling separate files from the test_deploy branch, because they are superfluous on the main branch which I use locally.



Solution 1:[1]

TL;DR

You probably can't do this the way you want. You might want to use cherry-pick, as Alexandr commented.

Long

If you think of Git as being about changes, files, and/or branches, this will lead you down the garden path. Don't do that: think about Git as being about commits, because it is. Each commit:

  • Is numbered. A commit has a unique hash ID. This is a (very large, random-looking but not random at all) number expressed in hexadecimal. Once Git has assigned that number to your new commit, it is now off limits for every other commit ever.1 If any other Git repository claims to have that hash ID, they must have your commit, not some other commit.

  • Is read-only: no part of any commit (any internal Git object, really) can ever be changd.

  • Stores two things: a full snapshot of every file (but compressed and Git-ified and, crucially, de-duplicated so that even if a million commits use a large file there's only one copy of that large file), and metadata. The metadata generally include the raw hash IDs of other commits (usually exactly one, the previous or parent commit).

  • Is found by its hash ID, which means you must provide Git with the raw hash ID, or with something Git can use to find the raw hash ID.

A branch name provides a raw hash ID to Git, so a branch name allows Git to find one particular commit. That commit then provides the previous commit's hash ID, so that Git can find two commits. The previous commit provides its previous commit's hash ID: this means Git can find three commits. That third commit provides another hash ID, and on and on we go.


1This isn't—cannot be—true forever, but if any other Git repository uses the same number for a different commit, you can never have your Git fetch from or push to that other Git successfully. The large size of hash IDs makes it plausibly true for millions er thousands urk tens of years (SHA-1 is no longer big enough and Git is moving to SHA-256).


Using branches

I use the main branch to make some changes locally ...

What this really means is that you use the name main to locate some particular commit—the latest commit "on" your main. You have Git extract the files from that commit's snapshot, so that you can read them—remember that only Git can read the Git-ified copies inside the snapshot—and write them (make your changes).

If and when you make a new commit, Git will:

  • save the updated files;2
  • save some metadata saying that you made this new commit;
  • include the current (latest-on-main) commit hash ID as the parent of the new commit;
  • write all of this out once, to a frozen snapshot copy that can never be changed at all; and
  • thereby allocate a new unique hash ID, which Git now writes into your name main.

It's the act of making the new commit that's the most important part. It's the fact that Git saves the new commit's hash ID under your name main that "changes your branch": your latest commit is now your new one, whose parent is whatever was your latest commit a moment ago. So this is how a branch "grows," one commit at a time.

The last thing to realize here is that Git is distributed: you have, in your repository, all the commits you got from some other Git repository, which you find by your branch names, or sometimes by your remote-tracking names such as origin/main. They have, in their Git repository, all the commits they have, which they find by their branch names. Their branch names are theirs; your branch names are yours. Keeping them in sync (whether to do it at all, and if so, how often) is up to you: your Git copies their branch names to your remote-tracking names and will update those automatically, but your Git doesn't update their branch names.

As you make new commits and update your branch names, nothing happens in the other Git repository—yet. That's where git push may come in:

So, generally, I wanna push changes from the main branch to test_deploy

Note here an important issue: there is not one (the) main branch. There is your name main (in your Git repository), their name main (in their Git repository), and your name origin/main (in your Git repository, remembering—whenever you connect your Git to their Git and make yours update your Git's memory—what they have in theirs).

What's shared between your two Git repositories is not the names, but rather the commits. When you first cloned their repository, you got all the commits and none of the branches: your Git turned all their branch names into remote-tracking names. Then, at the end, your Git made one new branch name of your own, using the commit hash ID your Git stored under your remote-tracking name: a rather long way around to make a branch-name of the same name holding the same hash ID, but that's exactly what it did. It's not one branch, it's two: both are named main but these in two different Git repositories. Adding commits to your main does not add any commits to their main, nor to their test_branch.


2You must first use git add on each updated file, for reasons not covered in this answer. You can use git commit -a to defer learning these reasons, but this will leave you in the dark about a number of other important Git things, so don't delay it too long.


Drawing branches

Before we talk about how git push works, let's draw some branches. Instead of giant hexadecimal numbers, let's use uppercase letters to stand in for commit hash IDs: they're much more human-friendly. (Of course we'll run out fast—that's why Git uses big numbers—but we'll keep our drawn branches tiny.) We'll start off with a main and a test_branch, which we'll draw like this:

...--G--H   <-- main
         \
          I   <-- test_branch

This is how things might look in their repository before you clone it. Note that the latest commit on main is H, and the latest on test_branch is later—it's I—but the parent of I is H. This means all the commits on main are also on test_branch. There's just one newer commit on test_branch than there is on main.

When you git clone this repository, you get only a main, not a test_branch: your Git changes their main to your origin/main, changes their test_branch to your origin/test_branch, and then creates your own main from their main:

...--G--H   <-- main, origin/main
         \
          I   <-- origin/test_branch

Note how two names identify commit H. That's fine! You can have as many names as you like that all point to one commit. You can, if you like, create a new branch test_branch now pointing to commit I:

...--G--H   <-- main, origin/main
         \
          I   <-- test_branch, origin/test_branch

The act of checking out or switching to main tells your Git repository to extract the snapshot from commit H—the one to which the name main points—and to make the name main the current branch, which we can draw by adding the word HEAD next to main. The git log command does this with colored text (HEAD -> main, especially in git log --decorate --oneline --graph output); I like to do this with main in parentheses:

...--G--H   <-- main (HEAD), origin/main
         \
          I   <-- test_branch, origin/test_branch

You now change some files in your working tree—which is where Git put the usable copies—and maybe run some tests or whatever, and then git add and git commit as usual. This makes a new commit which makes your name main point to the new commit. The new commit points back to existing commit H, like this:

          J   <-- main (HEAD)
         /
...--G--H   <-- origin/main
         \
          I   <-- test_branch, origin/test_branch

Note how nothing else happened: the other names are undisturbed, and no existing commit has changed: H still points backwards to G, I still points backwards to H, and new J points backwards to H too.

Now we can talk about how git push works. Remember that git push calls up another set of Git software that works on another Git repository. We'll use the short name origin, which stores the URL at which your Git software reaches that other Git software. You might run:

git push origin ...

where we'll fill in the ... part in a moment. Your Git will call up their Git and the two will converse about commits by hash IDs. Your Git will tell them about new commit J, which they won't have, and then tell them about commit J's parent H, which they do have. So they'll say please send J but I don't need H as I have that and every earlier commit too. Your Git can now figure out a minimal package that will deliver to them everything they need to re-create commit J in full, with commit J's same hash ID in their repository.

They therefore end up with this:

          J   [no way to find it yet]
         /
...--G--H   <-- main
         \
          I   <-- test_branch

Remember, they don't have any origin/ names, just their branch names.

The last step of your git push is going to be your Git asking their Git to set one of their branch names. You can choose an existing name—main or test_branch for instance—or a totally-new-to-them name. You'll have your Git send them a polite request: Please, if it's OK, create or update your name ________ to point to commit J. Let me know if that was OK.

Your Git will fill in the hash ID of commit J here based on what you give to git push. Your Git will fill in the blank for the name based on what you give to your same git push command. This determines what goes in the ... part:

git push origin main:new_branch

will send them commit J because the part on the left of main:new_branch says main, which means commit J in your repository, where main points to J. It will fill in the blank with the name new_branch because that's on the right of the colon in main:new_branch.

If you just run:

git push origin main

your Git will use the single name here—main—to find the commit in your repository (commit J), and also to fill in the blank for the name in their repository (main). So that would ask them to add commit J to their main.

You want them to set their test_branch to point to commit J, though, so you might use:

git push origin main:test_branch

This asks them to set their name test_branch to point to commit J.

But what if they did do that? They'd now have:

          J   <-- test_branch
         /
...--G--H   <-- main
         \
          I   [lost!]

They no longer have a way to find commit I. They can't use their main: that points to commit H. They can't use their name test_branch, if they update it, because it will point to J, which links back to H, which links back to G, and so on. Nobody links forward (ever, in Git) and so there's no way to find commit I.

In short, then, they'll say no to your polite request. You'll get a ! rejected and non-fast-forward error from your git push, which is Git's jargon-esque way of saying "if I do that, I'll lose some commits".

So what can you do?

The problem here isn't really branch names per se, but rather the fact that your new commit J links back to existing commit H, and that's what's wrong. You need a new commit that links back to existing commit I—the latest on their test_branch.

Note that before you begin doing any of this, it's advisable for you to run:

git fetch origin

This step has your Git call up their Git. They list out all their branch names and commit hash IDs—the same thing they did back when you first ran git clone—and your Git can now figure out whether they have any commits that you don't have. For instance, perhaps someone added a new commit K onto test_branch, so that they now have:

...--G--H   <-- main
         \
          I--K   <-- test_branch

If so, your Git will obtain new commit K from them—your Git can tell it's new to you, because you have no commit with that number—and will then update your origin/test_branch—your memory of their test_branch—to match.

Now (after updating) is the time to create your own test_branch, pointing to the same commit they have as the last one on their test_branch. If they're still on commit I, fine. If they've added commit K, or even dozens of commits, that's fine too. You'll end up with your own test_branch pointing to the same commit as their test_branch, which is the commit your origin/test_branch identifies after the git fetch.

In any case, whatever they have now, you can now copy your existing commit J to a new and slightly different commit—let's call this one J' to show how similar it is to J—that adds on to wherever their test_branch ends. You'll run git switch test_branch now to create it, or git switch test_branch && git merge --ff-only origin/test_branch to update it, or perhaps even git switch test_branch && git reset --hard origin/test_branch to update your test_branch:

          J   <-- main
         /
...--G--H   <-- origin/main
         \
          I--K   <-- test_branch (HEAD), origin/test_branch

Now you run:

git cherry-pick main

which directs your Git to find commit J—as found by your name main—and do its best to copy that commit. Copying a commit involves figuring out what the commit changed, i.e., running git diff, and then applying that change wherever you are now (on commit K in the drawing above) and making a new commit from the result. Technically, cherry-pick is a full three-way merge, but you can, if you like, think about it as a "diff and patch" process and you won't be too far off.

Assuming all goes well, the end result looks like this:

          J   <-- main
         /
...--G--H   <-- origin/main
         \
          I--K   <-- origin/test_branch
              \
               J'   <-- test_branch (HEAD)

A git show main, which shows the difference between the snapshots in H and J, and a git show test_branch, which shows the difference between the snapshots in K and J', will show the same set of changes (as adjusted if necessary) to the same files changed in H-vs-J.

You're now ready to run:

git push origin test_branch

which uses your name test_branch to locate commit J', send that commit to origin—they already have everything that comes before J' so you just send the one commit—and then ask them to add commit J' to their test_branch. Since that adds on to K, they'll probably accept this polite request.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1