Git is my buddy: Effective Git as a solo developer
mikkel.ca
mikkel.ca
Is this a terrible coding practice? I don't have enough non-me experience to know what an anti-pattern this probably is. I probably won't change my process, but I'm curious.
This for me is one of the biggest things I like about working under version control, even solo. It gives me the freedom to explore some crazy idea or refactor without having to think about the way back if it doesn't pan out. If it turns out to be more complex than I am willing to do now, I can stash or branch.
If I think back to my pre-source control days, I used to leave commented code everywhere, or just make a full copy of the folder. It doesn't take long before this becomes an absolute mess. Copying in particular was a barrier: you had to realize it was necessary then interrupt your flow to do something that would take a several seconds. (By contrast, if you commit as you go - especially everytime you get to a working "checkpoint" - there's zero extra effort needed.)
Take the section you are stuck on, print it out, cut it up into sentences or phrases, and just rearrange them until either it makes sense, or you figure out where you went wrong.
Rearranging code statements until something makes sense is exactly what refactoring is.
Refactoring is not merely rearranging code statements. Refactoring is restructuring of the code starting from the architectural and abstract goal and then looking at how pieces of existing code would fit. Sometimes, that requires writing new code and tests. Refactoring by definition also means not breaking the user space.
I've never heard of any serious writer printing out their prose and cutting it and rearranging it. That just sounds absurdly unnecessary to me.
you are either using a different definition of architecture or this is wrong. Refactoring is bottom up construction. Most of the time when I see people frustrated or struggling (including myself) it's because they have forgotten this and need to take a break.
More information here: https://en.wikipedia.org/wiki/Code_refactoring
There are many goals in refactoring, specifically this section:
> Potential advantages of refactoring may include improved code readability and reduced complexity; these can improve the source code's maintainability and create a simpler, cleaner, or more expressive internal architecture or object model to improve extensibility. Another potential goal for refactoring is improved performance; software engineers face an ongoing challenge to write programs that perform faster or use less memory.
I was addressing OP's analogy to cutting pieces of written prose and rearranging.
Code is meant to be read by humans and only incidentally by computers.
A lot of architecture is just being clear about what is intrinsic complexity and what is accidental, be it cognitive or computational.
Writers definitely do this. Maybe not at the prose level, but for sure at the plot level and chapter level.
I think a lot of people who haven't been around the scene wouldn't believe these stories, but this stuff happens a lot. Like major commercial projects with no tests whatsoever (unit, integration, or otherwise), that are still successful and making a lot of money.
I worked at a small "startup" inside a larger, several billion dollar company, back in the late 90's. Nobody set up source control for that division, despite the parent company being over two decades old and having people very experienced with that sort of thing. We were also integrating code from third party contractors, and it was a big mess. Files getting overwritten, people copying stuff off their local desktops, consultants FTPing in updates, etc. After a couple months of copying junk everywhere, I finally got fed up. As a 22 year old, basically straight out of college, I was training the entire team how to use CVS...
Started with SCCS with versioned control lists to determine what got pulled from SCCS. The outer wrapper was all written in shell. 1980s.
Talking about a large system, eight or ten sub projects, each sub project in its own versioned source tree.
A release spec pulled the SCCS deltas of all the sub project control lists, and then SCCS was directed by those versioned control lists to pull all the source code for each sub project.
So yes, version control that I am aware of was firmly entrenched in 1980. And I am certain it goes back further than that.
That said, it can be a lifesaver when I didn't make an explicit commit, then started doing stuff, then realized "ok this got out of hand AND I wish I could go back five minutes but it's gonna be annoying."
One of the failure modes for people leaving a mess is if it's too hard to fix it they give up. So that's no good.
Some people cut a trail through the jungle, others just push the branches out of the way and expect everyone behind them to do the same.
[1] https://fossil-scm.org/home/doc/trunk/www/rebaseharm.md
I think we often feel the urge to rebase and squash not because it actually makes our code changes easier to understand, but because it makes us feel better about ourselves. That's a red flag. Understanding how you got to the goal -- encoding all the fumbles and disoriented thoughts right in the commit history -- that can be a genuine benefit to the reader. Who do we really help by pretending that we're more organized, coherent, and linear than we actually were?
We're helping the future reader who's reading the history because they want to understand why a change was made - and "because the author of the branch initially had the wrong idea" is almost never the answer they're looking for.
I sometimes enjoy reading stream-of-consciousness writing, but most of the time (especially when reading code) I'm more interested in the point itself. The same applies to version history. It can be used to tell the raw story, but there's usually a more useful and interesting story to be told.
Five years from now, no one needs to know that I forgot to add that one line to a prior commit and had to add it separately, or that my first attempt didn't quite pan out as expected.
What that future person _will_ care about is:
- What final changes actually got made?
- What task was I working on?
- What was the reason for any of these changes in the first place?
- Why did I make some of these changes specifically to implement that task?
- What additional side info is important context for understanding the diffs?
Often my changes are
1. Refactor the existing code to support the new feature
2. Add the new feature
It's great to keep these separate, because someone can look at number 1 and see that the two versions of the code ought to be functionally the same (same tests pass, app looks the same, refactor is easy-to-understand), and look at number 2 and see the new feature.
There are countless other times where you want to tell the "story" in a logical fashion.
(Honestly, I expect that there is a significant correlation between being a good git committed and being a clear story-teller.)
This is especially true if your code is clever. I'm much more likely to understand your polished gem if I can see all the things that you bumped into while you were discovering it.
The problem with your request is that 90+% of the time (with the way I develop), the dead ends are on MRs that got closed or code that never got pushed in the first place. So again, comments as to why this approach is used is way better than hiding it in the history because someone coming to "clean up" code sees the thought process instead of having to remember to search for it.
Couldn't you simply review/bisect at the fork/join points? i.e., take the commits at which forks began or ended, ignore any intermediary commits, and run the bisect (or, read diffs) across that subset? That way you're only comparing at the chapter-markers of the story, so to speak, and not getting mired in the gory details.
Even with `bisect --first-parent`, I still want useful commit messages which "fixup" commits, again, are uniquely terrible at being on the whole.
I do software process and other things, so some of my branches tend to be gigantic (e.g., revamping the build system) and can be 200+ commits because one cannot meaningfully land a build system rewrite incrementally. That one in particular was meant to be bisectable because when rebasing on top of new development, I wanted fixes to be in the "port this library over" commit instead of after some random merge commit based on when I decided to sync up that week (it took a year to do it). So once I get it down to a particular MR, being able to inspect that topic is still a useful property.
Note that this only works with a `merge --no-ff` workflow too. The `rebase && merge --ff-only` pattern and `merge --squashed` are both terrible, IME, at making useful history. The force-rebase workflow is just as confounding to me as the no-rebase workflow (the former de-parallelizes your MR merge process and the latter tends to make a terrible commit history).
Note that even for single-developer projects I run, I tend to make PRs even for my own changes (once it's gotten off the ground).
What you probably want in this case is something like "automatically commit on every change (possibly recording every keystroke)" + "automatically tag based on tests/builds passing or failing" + "allow manual comments at any time, whether based on files changing or not". All of that is technically possible with git/hg/fossil/etc, but it's so much work for both the recorder and the viewer that it's infeasible.
Actually that's still important, it's just important from an architecture perspective.
I suspect this is the type of tendency which goes away with experience, but it makes git a lot less useful. My commits won't really tell you what changed; the most they can tell you is the primary change I was working on.
On the other hand, there's a diminishing return to placing every tiny change into a separate commit. Commit messages like "Fixed multiple small things" might make some people clutch their pearls, but sometimes you just need to get shit done and move on to solving bigger problems.
My suggestion is to consider breaking your commit into two: one for "fixed this big issue that everyone cares about", and one for "a bunch of tiny cleanup stuff that I happened to notice." (Maybe call that second one "refactoring" -- it will go over better with your audience.)
That assumes the changes are in separate files though, right? I know you can do use the "-i" flag, but it's fairly labor intensive.
(But easy or not, other version control systems such as Subversion don't offer the feature at all. We kind of take Git for granted these days, but it wasn't always like that.)
Up to you if you wanted to rebase those changes back onto main.
I don't use it often and find it's kind of painful to use, but if you're in the position where you've already saved two different things in your IDE and need to pull them apart for commit, it's a useful tool.
In general, more experienced git users aren't actually working on one commit at a time. They're just comfortable enough with editing history to make it look that way.
https://git-scm.com/book/en/v2/Git-Tools-Interactive-Staging
"Change" is a subject to interpretation. Most of the time it's the scope that the change belongs to is what has the meaningful value.
Say, changes made in connection to fixing an issue are logically tied for inclusion as well as for potential unwinding.
Some tangent changes technically should not be casually folded in, just in case this changeset will need to be propagated or rolled back.
Thus this elaborate muli-staged commit management in Git.
Many projects don't have such need to manange the change flow, so Version control is used as a kind of undo buffer. Which is fine, in such cases the meaning is tied to release states.
If anything, it makes more practical sense to preserve only commits with a buildable state, not just some transitional changes.
A squashed merge or rebased and cleaned set of commits gives a very clean overview of which changes where made, at what point, why they were made, and what together. That picture tends to get utterly lost in the "set up X", "make test Y", "fix typo", "wip" and "change error handling" commits a feature branch typically has.
Additionally I'm not really interested in that my colleague started change X yesterday before lunch, I'm interested in when it went live and became visible for the all developers when it was merged into the main branch.
[1] https://git-scm.com/book/en/v2/Git-Branching-Rebasing#_rebas...
That may be an accurate representation of the order savepoints were made, but it's not an accurate representation of how the software evolved. It is noise that needs to be discarded if a reader would like to know what change was really made. It also makes if difficult or impossible to use tools like git bisect.
Is the argument really that a more detailed history is always better? In the trivial case every keypress could be a savepoint, and every savepoint a commit.
One does not always know in advance that a commit needs to be split in two. The only way to produce readable commits without rebasing them in that case is to work with local _backup files. A version control system does this much better.
I love your question, "why not a commit per keypress?", because it raises an interesting follow-up: why not squash and rebase entire months or years of project work into single commits? If squashing is so useful, why do we only apply it at low-grain scales? Could we read and understand massive projects quickly and easily, if they only had a few commits to them?
I'm sure that we don't experiment with larger-scale rebases because of the limitations in the technology -- we all know that we're not supposed to 'git rebase' in public, and why that is. But suppose those obstacles were lifted. Now that we can rebase and rewrite at any time scale, which scale(s) is the right one(s) to choose?
The argument here is that one should rebase and carefully craft commits that isolates each functional change into a separate commit, where each change is motivated and builds on previous, before pushing anything. Every commit should build cleanly, preferably even pass tests. That makes changes easier to reason about, and enables the use of tools such as bisect. Look at git itself for an example of this type of history.
The counter argument to that was that it presents a false view of history. Maybe there were false starts and mistakes made along the way. Without preserving these to history the reader is left without understanding these. This is not an uncommon argument. Some people argue rebase should never be used.
This view suggests that a more detailed history is preferable. Taken to its logical extreme, that would mean every keypress and editor command.
But "why not delete all of history" is not an example of "carefully crafted commits" taken to an extreme. Quite the opposite.
A commit is essentially an email. It has a sender, date, a subject line and a message body. The commit message format is subject, empty line, body. Think of git repository as an archived mailing list worth of patches.
Much the same as you wouldn't send an email describing several days of work without proofreading it, you should treat your commits the same way. The git design grew out of this usage, which was much harder in something like Subversion.
No one would send an email to the kernel mailing list suggesting a patch set that included errors, false starts, and reverts. That would waste reviewers' time. Code history is a craft to aid understading the code, it is not an undo log.
That's effectively what happened before version control/before the small-scale rebases we enjoy now were possible. And the reason is that it's hugely valuable in certain circumstances to be able to see some granularity of the history. (Though clearly people disagree about what the grain size should be.)
> Could we read and understand massive projects quickly and easily, if they only had a few commits to them?
I don't think so. The current state is visible at the top of the git tree regardless. History comes in when you are trying to understand why the state is what it is. Usually this is for troubleshooting in my experience, but sometimes also when doing a refactor. Meaningful commit messages attached to meaningfully-clumped patches are, in my opinion, absolute gold in those cases.
I think the reason to squash commits is to cut out the noisy bits that were only useful to the original developer that day and create a timeline that's helpful for future readers. It doesn't really make sense to get more granular than the level of a single commit with a good comment and a small set of cohesive changes. So you store your history at that granular level and you can take care of the rest with tags, minor and major versions, etc.
"So, another way of thinking about rebase is that it is a kind of merge that intentionally forgets some details in order to not overwhelm the weak history display mechanisms available in Git. Wouldn't it be better, less error-prone, and easier on users to enhance the history display mechanisms in Git so that rebasing for a clean, linear history became unnecessary?"
I suppose the Github answer to all this would be "just make separate PRs", but going that way asks a lot more of the developer in terms of how polished those incremental states need to be.
https://www.mercurial-scm.org/doc/evolution/user-guide.html#...
It still has the individual commits, but the interface will make it appear as if it's just one commit.
I do think this is because I prefer to think of code as a black box. No one should need to figure out how my functions work. Someone should just need the name of the function, what inputs it receives, and what output does it return. If someone actually has to read my code, that’s a failure.
I can't tell if you're being serious, or are a brilliant troll. :)
Assuming you're serious, Hyrum's Law is one reason I might need to see your code (https://www.hyrumslaw.com/). The signature of your function is not the whole signature, it's just a sketch of the high points.
You help the reviewer.
To understand why git is the way it is, you have to understand the workflow of the original git-using project (other than git itself), the Linux kernel. Whenever someone proposes a change to the Linux kernel, it's sent as a sequence of patches. Each patch should contain a single logical change, and will be reviewed individually. For instance, suppose you want to change the way a field in a particular structure is stored. The first patch of your series might introduce a couple of helper functions to access the structure fields. Patches 2-5 might each change a separate subsystem to use the new helper functions, instead of accessing the field directly. The next patch changes both the field and the helper functions to use the new representation. When reviewing this sequence, it's easier to see that each patch is correct. And that was a simple example; it's not rare to have patch series with over 15 patches, and even longer patch series are not unheard of. I've seen patch series which refactor whole subsystems, where each patch in the series was an obviously correct transformation, while the final result was completely different.
This tired hyperbole just won’t seem to ever go away. Please try to ignore this junk, the Fossil devs could and should make their point without the FUD and misleading judgement, if they want to be taken seriously. Rebase has perfectly legitimate uses, and if Fossil makes it so you don’t need to rebase, that’s fantastic.
Rebase is most useful before pushing local changes to other people, and most people fluent in git know this fact, and also know that you don’t rebase public branches, you don’t rebase other people’s commits or your own after they’re pushed, except in emergencies and with team communication.
Rebasing before you push is the same amount of “lying” as typing something into your editor and then deleting it before you hit save. You don’t actually want your history at the raw keystroke level, right? You aren’t “lying” if you fix a bug you wrote before you push the bug into public branches, right?
Disagree.
Sorry, but I'd rather be rather inclined to read commit history like this: (whether it's reviewing others' code or my own at a later time)
- Add functionality X to function y()
- Fix a bug in y(): ...
- Fix a bug in z(): ...
than
- X
- oops
- fuck, typo fix
- do it another way
- ok, y is fixed now
- another typo fix
- it has a bug, fix it
- z has the same bug
- typo fix
Whereas the latter can be quite common during dev cycle so as to keep it to yourself. It's not about 'pretending' at all.
I'll offer an alternative. I love having every commit buildable. When I'm drafting, this isn't going to happen. I'd like to save my work and move between machines more frequently than that. But after a rebase, it's great to only have compiling commits. It makes doing a bisect a lot easier when you're hunting for something.
But basically, I let things "all hang out."
Tools shouldn't really be running the show.
The branching strategy means that it's pretty important that my commits are small, the brief commit message is accurate (even if I occasionally commit too many changes at once) and the description explains my train of thought. Nearly every time, I am communicating those changes to myself in 6 months when I switch into that branch randomly and wonder what I was in the middle of doing.
"In other words, you really shouldn't rebase stuff
that has been exposed anywhere outside of your own
private tree. But *within* your own private tree, and
within the commits that have never seen the light of
day, rebasing is fine."
-- Linus Torvalds
[1] https://yarchive.net/comp/linux/git_rebase.htmledit to add: I generally prefer people not rebase after they've asked for a PR review just because the reference for comments will be lost. If they want to, maybe do it after all the reviews are approved.
When I work alone I'm climbing a mountain, and Git is the rope. I can fall, but I won't fall far. I commit as often as I want to. The log is not a story for someone else to read later, it's the way I get to the top.
This kind of rule prevents people from maintaining the code base as they go. I have literally quit a company because of bullshit such as this. I was a senior engineer and could not fix a typo in a comment without a bug number and two code reviews.
Short circuiting the review and QA steps is not ideal. A reviewer should see the change is just a comment typo fix and accept it even if it has nothing to do with the current ticket.
You can extend it to remote-history too, because git makes it almost trivial to create a repo that you want to work over the network (without a running server of any kind).
I use git as a fancy rsync sometimes.
I do most of my work on a remote box, but I still like to edit locally in an IDE, but occasionally I make a change on the remote side.
On the remote side, I do
git init --bare project.git
git remote add clusterx remotebox:dev/project.git
Then do a git clone on the remote box from that repo, then I can push changes back to that local repo and when I'm done with the day, I can just pull it all back to my laptop with a git pull.
This used to be full of patch + diff + rsync in the past, but when you build stuff remotely and do diffs, but add new files to the codebase, it is so much easier to just use git.
For my personal projects, I think CSS files are the most common things I've edited in this sort of loop - my web-app folders are generally just git clone --depth 1, which also takes care of the other loop where I edit locally and deploy to remote.
And as another reply mentioned, squashing commits is good for keeping your history cleaner. My branches tend to have a ton of "fix" commits that get squashed out when merging into master.
If it's working for you to produce software how you need, then it's working.
But I would say it's building up habits of using git that would not transfer well to a multi-person team. That may or may not matter to you.
OP's usage is interesting, I think by and large they are transferable to a multi-person team, they are still good habits, or on the _way_ to good habits or _similar_ to good habits with a multi-person team. The one difference is how much easier it is for a solo developer to "rewrite git history" without disrupting others, in OP we see it done with abandon.
But in general the way OP is thinking about things -- what they are trying to prioritize how -- are things that apply to a multi-person team too. Keeping commit history readable, keeping branches cohesive, etc.
Your practices are... not. Which doesn't make them terrible, but it means you are developing habits you'd probably have to revise when/if working on a multi-person team.
It's a bit like testing - there's a lot of posts about where you need them and not many discussing where you don't.
More on topic, when coding the game, I was Git committing maybe every hour or so without useful commit messages and didn't have a problem. With games (in the early stages anyway), I find you're typically changing lots and lots of small things all over the place to tweak the gameplay and presentation in an experimental way, so granular commits aren't helpful.
I would switch to more granular commits now though since the game has stabilised more.
I only work on little solo projects and this is what I'm doing. It makes a very readable history, and helps me answer "why on earth did I do that?", but it's harder to revert small changes later.
If I'm working with others, I try to match my committing style to the project.
But at some point you are spending more time bookkeeping than the actual value you will get from it. If its a personal repo, don't bother. If you are sending a patch to Linus, tidy your commit messages.
So yes, having "interm" or "wip" commits would be an anti-pattern in a shared repo, as it makes it harder for others to see what changes you made. For a local branch though; not a big deal.
For solo development? I don't think it's good, but ultimately you should do whatever works for you. When you work on a team, though, it might be hard to break the habit later if this is what you're used to, and you really really will need to. Nobody wants to see "snapshot" commits in a shared repo; commits will need to actually accomplish a clear goal. Also, I find it very helpful to be able to make independent changes in separate commits (sometimes I see something wrong that's unrelated to whatever I'm doing), then reorder (rebase) them to polish them sometime before pushing. If you don't get in the habit of making your commits somewhat orthogonal, you won't be able to do these kinds of things (whether on teams or solo).
I think it's when you start syncing with other people over multiple days that people start insisting that a commit should (compile), be atomic, tested, etc. What they're really looking for at that point is that incoming changes be easy to understand and modular and possibly easy to omit if some code change is causing them trouble for a moment.
If it's working, stick with it. Most people use Git as a centralized RCS anyway. I like the decentralized features of Git (darcs, fossil, hg, whatever), but mainly for short-term problem solving -- on any project, eventually an official hub emerges.
Principle 2a: Every commit must include its own tests
Principle 2b: Every commit must pass all tests
But otherwise, I don't create branches. My commits are medium sized, one big thing, and to the trunk. My commit messages are at best ok.After a commit, I git --amend liberally. It's never really clear in my mind when a commit ended and the next one starts. This wouldn't fly in a group.
The one think I'd recommend is Never Type git. That's overstating, but basically git's command line syntax is just terrible AND thus dangerous. I think my one moderate sized git screwup was due to the command line syntax. So now I hide (most of) it behind shell aliases. This guy goes a bit far but you get the idea:
https://github.com/ohmyzsh/ohmyzsh/wiki/Cheatsheet
I pull rarely enough that I prefer to type it out.
Also, configure a good diff tool (although Apple seems to reject kdiff3 for now). And .gitignore goes without saying.
Are you the only consumer of the practice? And do you like it? Then no, it's not terrible at all, it's useful. Git will function just fine for this. I do similar things with my "experiment" repos, they're practically "streams of thought saved to disk" and they contain a ton of digressions and occasional breakages and that's totally fine. I have zero complaints after several years of doing this.
The major benefits to much-more-structured approaches come in the form of automated tooling that's really only useful when you have large repos or many contributors (git bisect is a perfect example), or external automation (ci/cd pipelines, etc). For those kinds of repos, yeah, I'd say it's a terrible practice, and it'll cause some easily-avoided pain. But even then: work however you like on a branch, and merge (or squash) when you have "good" stuff, and it generally works well.
Generally I am against rewriting history unless there is a big mess to fix. For me, git is my work process, and bugs, typos, bad merges and code that doesn't compile is part of it and I don't try to hide it. Personally, I value historical accuracy more than cleanliness.
But some people have compelling arguments for the opposite, like the author. These people tend to view git as a release schedule where every commit is workable code. It is good for bisecting, and git log is your actual changelog. But you lose information about how you solved problem, when you did what, etc... it is also more time consuming to maintain.
You can use a hybrid solution with two parallel branches and merge commits, or you can just use tags.
Git is not very opinionated on how you should work. Merge or rebase, clean or historically accurate, push or pull, etc... There is more than one way to do it.
Nope. I have been versioning everything from C# to SQL for more than a decade and it saved me many, many times. With Subversion too, which is far less evolved and modern than Git.
The advantage of mastering a complex tool like git and mantaining a central repository is the increased granularity of commits/branches and clarity of versioning, but if the "snapshot here and there" approach works for you, then use it.
Some of the other is best practices which you are missing out on.
You might want to take a look at what some 'best-practices' are and see which might improve your coding.
Simple things like tagging a commit as "feature/fix/refactor/chore" might make you think differently about your programming workflow. Or you might find it more of a distraction and limitation than a help.
and yes, sometimes you certainly need that 'interim' tag to freeze work. For those rare cases where you run out of time or inspiration before you get to a natural end point of a task.
When I'm really getting going I flow through a ton of work and only stop when I hit a time limit, so I expect to finish in the middle of a task whenever I start coding.
I think what'll get you in trouble more than having perfect process is writing spaghetti code that violates separation of concerns. If things are separated well, you should be able to come back and test it easily if it causes issues. Test the stuff that is complicated and obviously will cause issues if its not perfect. Test the stuff that is found to be buggy or needs to be proven to be not buggy in order to track down bugs. Don't bother with perfectly testing everything.
I've been adding a threaded AVL tree implementation lately. I definitely tested that extensively and did a savegame when the AVL tree was written and passing tests properly, and then added threads and did another couple of savegames. I'm going to build on top of that, and I need to be able to trust it without falling back into debugging it. I've got a Clamp01 function though which takes a double and ensures it is within 0 <= x <= 1 and I don't have that one tested. I'm pretty confident it works though.
What I wondered was if and when other developers deviate from that workflow. After the first release? After the first collaborator has joined? Never?
Are there textbook developers who use a strict strategy like the one Daniel Stenberg [1] is following from day 1?
[1] https://daniel.haxx.se/blog/2020/11/09/this-is-how-i-git/
I’ve definitely had situations when I had reverted code to a year back to check if there was a bug. Git was very helpful from that point of view.
My hobby project has been to extend a program with a new plugin, but then along the way I found bugs in the core code, and I wanted to upstream the fixes (and the plugin). I am very glad I knew what the fixes where, becasue I more or less had to rebase to the original code in order to untangle the mess of commits I made.
I have also found other folks wanting to use my code, so it also made it much more helpful if ourside folks can see how I altered the original program.
Another good trick trick is to simply stage things to "freeze" them—You can then `git checkout` any changed files if you want to revert to the staged state. This is useful if you are in working state without a lot of changes but want to run a quick experiment before committing.
Going a step further and rebasing interactively to tidy up your logs would also accomplish the same goal, but if it's just for your own eyes, it's probably not worth the time.
It is not. I do the same.
I tried to use git the way OP describes and I was taking more time to manage the logistics than to code.
I then hit the "I can obviously call a function in a new commit from one in a previous one". Handling this gracefully means full time work.
My code works 70% of the time anyway si I ended up making efforts to move it up a few percent points than to have a byzantin git tree.
Not at all. Copying folders with names like code1, code2, code3 is terrible practice. Using SCM and committing as checkpoints while you work is good coding practice. You have a process, and it works for you.
I'd prefer something close to his if I am writing a library or a small app.
From my perspective, a lot of this kind of rigid process is important and valuable when you are working with a team, but is counterproductive when it's just me. I know the tradeoffs of the corners I cut. I have the experience to know, for example, that's it's not a safe assumption that I will understand my own code a year (or even a month) from now. But the solution to that is to throw in some freeform comments that jog my memory; not to implement a heavyweight documentation system.
Everyone should work in the way that makes them the most productive and happy, but I don't think it's a good idea for solo developers to bring in practices that are designed for team collaboration without really understanding what value they will get from the extra effort.
It wasn't tool long before source control was missed - at it's simplest, it acts like a backup, but having the full history is really useful. Once familiar with SVN (this was 15 or so years ago), it really didn't add any overhead at all.
I thought tests would he too much work to maintain - but looking back, it was clear that it was my lack of experience and understanding that was the problem. The way I was writing tests, laden heavy with mocks, and brittle as a result, was resulting in crap tests. Much later, when I'd finished making terrible mistakes, but had learned a lot from the process, I realised how helpful tests were. I believe they are usually a net benefit - the number of times where I write a test for something that seems really simple, and it fails, is higher than I'd like to admit. Failing fast during dev is far prefer to having to diagnose production issues. And it also means I can refactor with much more confidence. Also, for OSS projects, I think the presence of a good test suite inspires confidence in users.
When working solo, the time taken for manual tasks also irked me - especially when preparing final releases, where I was anxious not to get things wrong - building for different platforms, creating installers, putting together configs, putting together docs, code signing, running things through Virus Total, publishing binaries... it took soooo long, and was error prone. In the beginning, automating this kind of stuff seemed like a mountain, but again with experience came competence and confidence. And the time savings were great, and the extra confidence around releases was huge.
Each to their own of course, but for anything non-trivial I wouldn't be without source control, tests or build automation.
With personal projects, the most important thing to maintain is interest and momentum. Best practices aren't so useful if you end up hating working on your project because of self-inflicted process.
Certainly, if you are using your project to improve best practices or learn "correct" ways of doing things for some other long-term career benefit, go for it. What I've learned, however, is that personal projects where I've spent more time doing "meta-work" were the ones where I never shipped anything or just spun my wheels feeling proud of the form of the project. The projects where I just threw caution to the wind and cut corners strategically (not everywhere, mind you) were the ones where I ended up shipping something.
I was a solo developer. We "transitioned" rather abruptly to the kind of workflow you would expect from an organization with hundreds of developers once we hired a couple more programmers, despite it being a poor idea, because we were still spread out among so many different projects. In retrospect, all of this turned out to be resume-polishing and practice runs for one of the developers and my manager; they blasted off to large organizations rather promptly.
All of it left a bad taste in my mouth and some rather negative feelings associated with git, which certainly are not helped by git's porcelain. There's an element of cargo culting against the practices of big SV organizations, but there's a very long tail of solo developers out there, and figuring out where you sit and what tradeoffs are required can be tough against the constant din of The Way Things Are Done.
But, as I spent more time programming, I developed different Git habits for different situations:
- Solo explorative work: working on a feature, many small commit messages create nothing but noise. Trying to come up with wording for these commit messages is mostly a drain on my mental energy: there is an infinitesimal chance I'll need these commits later. In such cases, I prefer to maintain large "checkpoint" work-in-progress commits that are the result of near-continuous `git commit --amend`s so that I can use `git status` to see what's changed since my last checkpoint, and easily revert to the last checkpoint. If this fails me, I can almost just as easily refer to the reflog to find some changes. The reflog, in my opinion, is an extremely under-utilized tool. `git diff HEAD@{2}..HEAD@{0}` allows me to see what's happened in the the last two times that I updated my checkpoint. Since this is active work I'm doing right now, I have a good sense of what happened and when, all without tiresomely writing commit messages that will be a bunch of gibberish in two days. When I'm satisfied with with the work on a checkpoint commit, I reset the branch to the previous real commit, and then incrementally create real, meaningful commits from the accumulated work, that have some chance to be useful to me in the future.
- Working on patches and bug fixes for live systems or production libraries: small, atomic commits that include tests for the patch are the only way to go. I believe the "correct Git" approach that is widely espouses is targeted to this use case, but not explorative feature work. Or maybe that's just me.
Wondering if anyone else codes like this.
An overly clean git history for me is a sign of too much perfectionism and greatly reduced productivity.
When I code I usually have a general idea of the stuff I want to include in my branch, but then I stumble upon bugs or code couplings which I need to fix for my feature to work. And then I include the fix into my feature branch, because it's just tedious to switch all the time and create 5 interdependent branches that need to be merged together anyway. Also as long as the feature branch itself is fairly clean then I don't give a rats ass about atomic commits.
And commits having to pass tests is just ludicrous. That's what the tests are for, so you can fix it before merging the branch. Don't go crazy on the commit level..
It depends a bit on the project, and how public it is. But in the end your git history never provides any benefit to customers and doesn't make your code better by itself. I hardly ever rewrite or rebase commits unless there's a good reason for it.
As a developer who works with code base that's got a git history going back over a decade and lot of legacy parts that haven't been touched in a long while, written by devs who've long since left, I fairly frequently wish they'd been more careful with the commit history.
You do mention project context as being relevant, but if bug fixes and refactoring _can_ be decoupled, it's a kindness to pull-request reviewers (if there are any) to do so and keep PRs small. It's also helpful if something needs to be rolled back if the commit history is fairly coherent.
I'll just say that the emphasis should be on clean branches and PR's much more than clean individual commits. And on good code much more than clean git branches.
If there's too much ceremony around branches and pull requests I tend to avoid small fixes because it's just much work and that definitely doesn't improve the quality of the code.
Even if you don't set up a public repository, having a remote one can be a good idea. The .git folder is surprisingly easy to trash. Any box with SSH access works, and GitHub/GitLab offer private projects that you don't have to configure further than a name.
My latest mistake was running 'rclone sync', which it turns out deletes files missing from the source without confirmation contrary to rsync.
This way, if you skip all the WIP commits you'll have mostly runnable code, and if you want to review a PR commit-by-commit it's easier to see what was being attempted in each commit than to only look at the feature as a whole.
I have never in all of my years working solo on projects needed to revert my history to debug a problem (excluding CTRL+Z of course!). If I am experimenting with something that I don't think will work, then I use a branch.
The amount of work to maintain a clean history and disciplined git practices is not worth it. For non-solo projects with even just 1 extra developer, then totally. Otherwise, you're just wasting your time...IMO.
But honestly, the nice thing about solo work is that you can do whatever you want, and confidently ignore people who think they know how you should work better than you do. If this helps you be more productive or organized, then go for it. And worst case scenario, you're less productive with your project, but you become a git wizard.
Large squashed commits on the main branch, each representing an approved and merged PR, which allow for a reasonable history of features and fixes.
The reason is that the SCM is not just a tool - the history of the codebase follows the thought process of the developer.
More structured history == more structured thinking. Cleaner history == cleaner thinking.
Documentation (traceability etc.) is certainly a benefit, but in this sense, it's part of the smaller picture.
Having every commit in a branch pass all tests is a little overkill, but gating merges to master/main on tests passing and then having branches squash before merging seems like a happy middle ground. At least that way every commit in master/main has passed tests.
I’ve found it useful because it’s in a separate context/UI (GitHub web view) as opposed to your code editor or even git diff on command line.
I use it for a few things:
1. As a collection of notes describing what was in my mind when I wrote that code 2. To allow me to work on different machines. For example I have an app where some of the work I do is directly on my staging server (long story). It's nice to be able to commit and pull there and to my local workstation 3. So I can rollback to known good states. Doesn't happen often but has saved me a few times over the years!
Messy execution exploits top-of-mind opportunities without permitting administrative overhead chores to distract.
Immediate meticulous cleanup constructs an idealized legible history, with the advantage of familiar recency.
The human brain itself works this way, consolidating long-term memory overnight during sleep.
The next question is how best to implement this workflow in git.
One option would be to use complex arcane git commands to transform a messy actual work history into an idealized legible official record. Even if the user performs this transformation perfectly, at minimum it causes a loss of information about the actual work history, by altering messy commits.
Therefore it's better to write completely new commits for the official record. One's idiosyncratic work history doesn't belong in the public collaboration git repo.
I find it easier to use separate git repos, one personal and the other collaborative. I transfer info between them only via manually syncing the working trees.
It may take syncing from multiple personal commits to update the official record sensibly, which sounds like a burden, until compared to the alternative of trying to understand a mysterious ancient official commit embracing multiple unrelated changes.
Code spelunkers unsatisfied with the terseness of the official record should be free to investigate the contributing dev's personal repo to sort through his chaos for clues.
If I’m making a very small change, e.g., a fix, I’ll work on master directly (personal work only! Shared work always uses the way we’ve agreed to work!).
A slightly more complex change or an exploration or experimental change will get its own branch.
A very complex change will have a base branch and feature branches off that base, possibly with issues, one per feature, merged into the base branch, which will eventually be rebased on then merged into master.
The only downside I found, I need to reboot the server for the django app to update to the changes. So I take my site offline for 3 minutes or so.
Docker makes it so easy to deploy and operate programs that I even use it for ecosystems that don't "need" it, like Java. Also makes backups super easy because it's just backing up the docker volumes.
* You can use your production server as a "git server" without basically any overhead
* You can set up scripts that run on git events.
Basically, you can push directly to your production server and then deploy using that those event listeners
Here's a better explainer
https://tqdev.com/2018-deploying-with-git-push-to-production
As for having to reboot your server every time, I have no idea, although that seems like a long time. I'd expect less than a minute, but I don't know much about django
Check out bitbucket pipelines that trigger on a commit which should be able to build/run any tests, copy the code to your server, and then restart django.
From there you can later build a docker image and copy it to run wherever, etc...
I'm sorry when I have to squeeze as much as possible into the limited time I get for my side projects after a long day of work as a professional developer - I have one rule: get stuff (that matters) done.
My home projects went from SCCS to CVS to SVN to GIT over the years. But there was always some form of version control, even for my home projects. I generally followed what I was using in day to day work, but with out all the process modeling, just the base version control.
Briefly in the mid 1990s I also dabbled in 3DFS and Plan9 for date based file systems. Those sort of negated the need for explicit version tracking systems, but neither idea endured the test of time.
In general, the best advice is to avoid nested git projects as much as possible (even though tools like git submodules exist, they are more footguns than you want them to be). You either want to reorganize your folder tree so that your git projects are only ever side-by-side rather than nested, or that there is only one repo for all of them ("monorepo"), depending on your preferences and when/how you expect to share them.
Just create a Git submodule, and then push from within the submodule, like so: https://stackoverflow.com/a/5814351.
Five years ago I inherited a project that had four repos sharing a Git submodule.
Now I tell people to never use Git submodules.
There's also just "meta" which is a cleaner way to approach the functionality of submodules https://github.com/mateodelnorte/meta.
Mid-reply context preserved below.
----
I have a git-inited folder A, called 'Build my web app'. Inside it, it has non-source-code stuff like pictures, pdfs, notes. Also inside it is a folder B, called 'my-web-app-js', which is source code.
[Problem was that making a commit in project B would trigger unexpected changes in A]
Your 'B' use case sounds like one we have right now: A framework, if you will ('A') and apps delivered via that framework ('B', 'C', etc.). Eventually, we will deliver A as a package and the apps as plug-ins to that package (more or less) but right now they all live together (npm run start in A brings in the apps in B, C, etc.).
Our top-level folder has a .gitignore that looks like this:
*
!thisProjectFile.js
!that.css
!images/\*
!otherArtefacts/\*
where the items preceded by ! are all in the A repo, things we want git to consider when in A but not in B, C, etc.In A's top-level folder, do a git clone of the repos for B, C, etc., and their folders will be created in that folder, but
1. All git operations in A will ignore B, C, etc., because of the .gitignore, and
2. Operations in B, C, etc., will ignore A, because it is "outside"
Works well enough for now.I must try this.
[1]: Pretty much the usual gitflow with sequential commits of partial work on a branch—often with commits fixing previous errors in the branch—until I deem it finished.
I recently wrote an extended post that aims to help devs understand how Git works, available Git commands, and techniques for using Git effectively:
https://blog.isquaredsoftware.com/2021/01/coding-career-git-...
I have an alias `alias diff='diff2html -s side --ig package-lock.json'` which shows a side-by-side comparison of my changes. Highly recommend!
As a solo developer/potentially newbie, I think it's better to spend brain cycles on actually learning programming, than to learn the idiosyncrasies of some crazily complex tool like git.
Doesn't look too hard to me. Particularly when I'm going to rewrite my system, I'll go ahead and check out a new Branch so I don't freak out when I break everything. Infact I tell a developer to learn git first.
The only time git becomes an issue is when you have large binary objects, like with a video game. Git LFS is pain.
The crux with git is that you really do have to learn it in depth, if you want to be self-sufficent in the end.
You don't though. I was using git for years at a 2 developer shop and I don't think I used anything but commit, pull, push, add. It was very trivial, never even made a branch.
Then when I moved to a bigger place I had to learn about cherry-pick, revert, reset, bisect, checkout, etc but every single one of those things I learned was immensely useful.
I found get to be an extremely useful tool when I needed to go back and look over some of the changes I made.
Git Wizardry: Obscure but useful Git incantations https://legends2k.github.io/note/git_nuances/
When developing only with yourself, git quickly ends up becoming a cloud backup. It shouldn't but...
There are times when I have three (or more) stashes stacked up on any given branch. I know I have reached a cohesive set of changes when I am ready to 'git stash drop' every one.