Pushing changes to branch other than main

3.7k views Asked by At

I cloned a repo from github I made, I did some changes. My repo on github has 2 branches, main and dev. I want to push my changes to dev first and then main.

I ran:

git branch

while in the cloned (local) repo, giving me the output:

*main

I don't see my dev branch I made on github.

How can I push my changes to the dev branch?

4

There are 4 answers

0
InfoDaneMent On BEST ANSWER

Apparently,

git branch dev
git checkout dev
git pull origin dev
git push origin dev:dev

worked.

0
knittl On

git branch only shows local branches, to show remote branches use git branch -r (to show local and remote branches, use git branch -a).

You can push any local to branch to any remote branch, by specifying the source (local) and target (remote) branch:

git push origin main:dev

will push the commits on your local main branch to the remote dev branch in the repository "origin".

1
torek On

As knittl said, git branch shows your branch names. Some other Git repository, such as the repository over on GitHub, will have its own branch names.

What's important in Git is not actually the branch names. These are for you, not for Git. More precisely, they exist so that Git can help you find the commits you want. This is because Git is really all about commits, not about branches—though we organize our commits into branches—and not about files, although each commit holds a snapshot of files.

This means that when you use Git, you must think first and foremost about commits. You need to know, at a sort of gut level, exactly what a commit is and does for you. Otherwise all the crazy things that Git does will be frustrating and impossible, and you will live in this xkcd comic.

Git is all about commits, so what is a commit? What does it do for you?

Each Git commit:

  • Is numbered. Each commit has a unique number, expressed as a hash ID (or object ID) in hexadecimal. This is the commit's true name, without which Git cannot find the commit. (For fun, see also the TV trope.) Hash IDs are large and ugly and impossible for humans to remember (or in most cases, pronounce ), so Git lets us use shortened versions sometimes.

  • Is completely, totally, read-only: frozen for all time. That's because the number, once assigned, means that commit, and it means that in every repository. When your Git repository sends this commit off to some other Git repository, that other Git will use the same number. It will never use that number for any other commit, not even before you make that commit. (This is where the magic is, in Git, and if you know hashing theory, cryptography, and so on, you'll know that this literally can't work. Someday, Git will fail. The sheer size of the hash ID pushes that time as far into the future as we need, or at least, we hope it does.)

  • Contains two things: a snapshot of all of the files that Git knew about at the time you (or whoever) made the commit, and some metadata, or information about the commit itself, such as the name of the person who made it.

There is a bunch of information in the metadata for each commit, but the key item for Git itself is that each commit contains a list—usually just one entry long—of the raw hash IDs of the previous commit or commits. Git calls these the parents of the commit.

The snapshot in each commit freezes every file for all time, so that anyone who has the repository can get back any version of every file. These files are stored in a special format that only Git can read, and literally nothing can write, and they're all de-duplicated within and across commits, so the fact that most commits mostly re-use the files from earlier commits means that most commits take very little real space. If you make a new commit that completely re-uses old files—this is possible in several ways—then the new commit literally takes no space at all for the files, and just a little space to hold its own metadata.

The snapshot means that a commit gives you the power to view every file exactly as it stood at the time you (or whoever) made the commit. But you can't use the committed file as a file, because it's in a special Git-only format, and you can't write to it as your computer would like to. So this means that to use a snapshot, you have to have Git extract it. We won't get into any of the details here, but this is what git checkout or git switch is doing when you switch to a particular commit: Git extracts the committed files, as if from an archive (because they are in an archive). You then work with or on the extracted files, not the files stored in Git. That means when you're working with files, those are files that are not in Git. You'll eventually have to make a new commit, to store a new snapshot into Git.

Meanwhile, the metadata gives you several things:

  • It lets you know who made the commit. The git log command will print the user's name and email address, and a date-and-time stamp. (With --pretty=fuller you'll see that there are actually two of these per commit; this is partly leftover from Git's early "everyone sends patches via email" usage.)

  • It tells you what they wanted to tell you about why they made the commit: this is their log message. The point of a log message is not to say what they did—Git can show that by comparing the snapshot in this commit to the snapshot in this commit's parent—but rather why they replaced i++ with i += 2 for instance. Did it fix Bug#123? Was it a feature enhancement? That sort of thing can go in the log message.

  • Using that parent metadata, Git can string the commits together, backwards. This is the history: this is what happened in this project, over time. By reading out the latest commit, we find the current source snapshot, and by using its metadata, we find its earlier parent commit. Using the stored hash ID, Git can now show you the parent commit. That commit contains the hash ID of a still-earlier grandparent commit, so Git can now show you the grandparent; that commit contains a still-earlier commit hash ID, and so on.

This means that the commits in the repository are the history in the repository. To access all the history, Git needs the latest commit.

Branch names help you (and Git) find the latest commit

Let's draw some commits. We'll pretend we have a tiny repository with just three commits in it. They'll have three random-looking, big-and-ugly hash IDs that nobody can remember or pronounce, so we'll call them commits A, B, and C instead. Commit C will be the latest, so it will have, in its metadata, the actual hash ID of earlier commit B. We say that commit C points to commit B, and we draw that like this:

    B <-C

But B is a commit, so it has a list of parent hash IDs—just one commit long again—which means that B points to its parent A:

A <-B <-C

A is also a commit, but, being the very first commit ever, it has a list of no parents (an empty list of parents) and doesn't point backwards. That's how git log knows to stop going backwards: there's just nothing left.

But: how does Git find the correct hash ID to extract commit C so that you can work on / with it in the first place? Remember, the hash IDs look random. They are unpredictable (because, among other things, they depend exquisitely on the exact second at which you make the commit). There's only one way to know the hash ID, and that's to have it saved: written down somewhere. We could write them down ourselves, and then have to type them in over and over again, but that's no fun at all. So Git stores them for us. This is what a branch names is. A branch name simply stores one hash ID, namely the ID of the latest commit.

If we have one branch named main, then, and C is the latest commit for main, then the name main holds the hash ID of commit C. As before, we say that this points to commit C, and draw it with an arrow:

A <-B <-C   <--main

At this point I like to get lazy and stop drawing the arrows from commit backwards to previous commit as arrows, because of what's about to happen to the drawings and because I don't have a good "arrow font" available. This is OK because once we make a commit, the backwards-pointing arrow from that commit to its parent is frozen for all time, like all parts of any commit. It has to point backwards, because we don't know what any future hash ID might be, so we just know they point backwards:

A--B--C   <-- main

The arrows coming from branch names, however, change over time. Let's make a new commit on main, by using git switch main or git checkout main to select commit C as the commit we'll work on / with, and then doing some work and running git add and git commit to make a new commit D, like this:

A--B--C   <-- main
       \
        D

New commit D points backwards to previous commit C. But now D—whatever its real hash ID is—is the latest commit, so the name main needs to point to D. So the last step of git commit is that Git writes D's hash ID into the name main:

A--B--C
       \
        D   <-- main

(and now we could draw the whole thing on one line again).

What happens if, before we make D, we create a new branch name, say dev? Let's draw one in and watch what happens:

A--B--C   <-- dev, main

We make our new commit D:

A--B--C
       \
        D

Which name gets updated? The answer for Git is simple: Git updates whichever branch name we have checked out. So we need to know, are we using the name main now, or are we using the name dev now?

To remember which name we're using, we'll add the special name HEAD to exactly one branch name, like this:

A--B--C   <-- dev, main (HEAD)

This means we're using commit C, but doing so because of / through the name main. If we git switch dev or git checkout dev, we get:

A--B--C   <-- dev (HEAD), main

We're still using commit C but now we're using it through the name dev. If we make our commit D now we get:

A--B--C   <-- main
       \
        D   <-- dev (HEAD)

There are now two latest commits: C is the latest main commit, and D is the latest dev commit. Note that commits up through C are on both branches, and the fact that D is the latest dev commit doesn't interfere with the fact that C is the latest main commit.

Suppose we switch back to main (and draw dev "above" instead of "below" if we like):

        D   <-- dev
       /
A--B--C   <-- main (HEAD)

We've gone back to using commit C, so we see the files from commit C, rather than those from commit D. If we now create, and switch to, a new branch named br2, we get this:

        D   <-- dev
       /
A--B--C   <-- br2 (HEAD), main

We're still using commit C but now we're doing so through the name br2. If we make a new commit now we get:

        D   <-- dev
       /
A--B--C   <-- main
       \
        E   <-- br2 (HEAD)

This is what Git branches are all about. The name finds the latest commit, and from there we / Git work backwards. The set of commits that we find as we work backwards is the set of commits that is "on" the branch, and that is the history of that branch.

The names find the latest commits, by definition. This allows us to "turn back time" by forcing one of the branch names to back up a commit or two or three, too. We do this when we use git reset --hard HEAD~1 to "erase" a commit: it doesn't actually go away, we just pretend we never made it by making sure that we can't find it with the branch name. For instance, if we're on br2 and make a bad commit F:

        D   <-- dev
       /
A--B--C   <-- main
       \
        E--F   <-- br2 (HEAD)

we can use git reset --hard HEAD~1 (or git reset --hard hash-of-E) to get this:

        D   <-- dev
       /
A--B--C   <-- main
       \
        E   <-- br2 (HEAD)
         \
          F   ??? [abandoned]

Then we can make a corrected commit G:

        D   <-- dev
       /
A--B--C   <-- main
       \
        E--G   <-- br2 (HEAD)
         \
          F

Since there's no way to find F, we'll never see it again, and it seems as though it's gone. (Git will probably, eventually—after 30 or more days—decide that we really don't want it, and drop it entirely, but commits are hard to get rid of. If you did save the hash ID somewhere, on paper for instance or in a file, and feed it to git show for instance, you may find that commit F is still there. When and even whether Git really does away with it is deliberately kept a bit of a mystery.)

Multiple repositories

A Git repository consists mainly of two databases:

  • There's one (usually much larger) that holds commit objects and other supporting objects. Git calls this its object database or object store and Git needs the hash ID to find objects inside this database.

  • Separately, there's a second (usually much smaller) database of names—branch names, tag names, and many other names—that Git uses to find the commits and other objects. Each name holds exactly one hash ID.

We can connect one Git database to another. When we do so, the two Git software packages use the hash IDs to exchange objects. Both repositories will use the same hash IDs for the same objects, so they can tell which objects the other one has just by comparing hash IDs. Then one Git—whichever one is sending stuff—sends over just the objects the other one needs, using the objects it already has to avoid sending stuff it doesn't need.

In this way, the two repositories wind up sharing commits. They literally have the same objects with the same hash IDs, so they share the commits with each other. Both have a full copy of everything.

Branch names in this names database, however, are specific to this particular repository. We may have our Git show them to some other Git, and that other Git can grab those names and hash IDs and do something with them, but they're our branch names. Tag names in this database, we try to share: if some other Git repository also has tag names, we try to use their names as-is, and share our tag names with them as-is, so that v1.2 means the same hash ID in both repositories. But branch names aren't shared this way! Each repository has its own.

Instead of sharing branch names, then, when you run git fetch or git fetch origin, you're telling your Git: Call up their Git software, have it connect to their repository, and find me all their latest commits via their branch branch names. Then bring over all the commits. Take all their branch names and change those into my remote-tracking names. Their main becomes your origin/main; their dev becomes your origin/dev; and so on. That way, whether or not they have added commits to their branches, your branches aren't disturbed. You get any new commits they have, and your remote-tracking names remember their latest commit hash IDs.

But this is not true for git push.

"Non-fast-forward" errors from git push

When you run git push origin or git push origin dev, you have your Git call up their Git software and repository just the same way you did for git fetch, but this time you choose to have your Git send your new commits to them. Instead of having your Git read their branch names and find their new-to-you commits, you have your Git send over hash IDs and find your new-to-them commits. Your Git then hands over these new commit objects to them ... and then asks or commands them to set one of their branch names. They don't have remote-tracking names for you! You just ask them to set their branch names directly.

Suppose you have:

        D   <-- dev (HEAD)
       /
A--B--C   <-- main

in your repository, but they acquired some commit E in their repository like this:

A--B--C   <-- main [their main, in their repository]
       \
        E   <-- dev [their dev, in their repository]

Your Git sends over your new-to-them commit D and asks them to set their dev to remember commit D.

If they did that—which they won't—how will they find their commit E? Remember, their Git is going to use their branch names to find the latest commit. If their dev moves to locate commit D, and D doesn't lead to E—and it doesn't—they'll "lose" their commit E.

If that kind of thing is going on, they will say: No, I won't set my dev to remember D as the latest commit, because that would lose some other latest commit(s). This shows up at your git push as:

! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to ...
hint: Updates were rejected because a pushed branch tip is behind its remote
hint: counterpart. Check out this branch and integrate the remote changes
hint: (e.g. 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

When this happens, you need to:

  1. run git fetch to get any new commits they have that you lack;
  2. inspect your and their commits and the relationships (parent, child, sibling, etc.), that these commits may have to each other: use git log with various options; see Pretty Git branch graphs
  3. rework your commits if and as necessary, e.g., using git rebase;
  4. repeat your git push now that you've corrected things, or use git push --force-with-lease if you're sure that you are supposed to tell their Git repository yes, lose those commits!

This is a lot of stuff to know. But if you're going to work with Git and distributed repositories, you need to know it. It should all be at least vaguely familiar, and the concept of commits and what they do for you should be very familiar.

0
Olasimbo On

You can try this:

git fetch
git checkout dev
git add .
git commit -m "your commit message here"
git push

git fetch will get an update of all existing branch from your remote repo into your local. Then, you can do git checkout dev to switch to dev branch. Then finally commit and push to dev

Then to push to main, you can make a pull request, approve it, and merge to main.