git-draft
is a new way to manage changes to your git
worktree before committing.
It replaces the git index/staging area with the concept of “draft commits”. Its philosophy is best summarized with the phrase:
Stash, don't stage!
To git-draft
, the index is irrelevant; the working directory is the only source of truth.
Instead of staging the changes you want, stash the changes you don't want, then commit what remains.
Think of it as a workflow built on top of git add -A
with extra features,
or a less-confusing version of git stash
.
The advantage of this approach is that it makes sure the commit will always match the workdir. This way things like tests and pre-commit hooks always see the actual contents of the commit without other unstaged changes mixed in.
The disadvantage is that you can't keep things lying around your working directory without committing them.
The hope is that git-draft
makes it easy enough to stash changes that this isn't a problem.
For some purposes, the .git/info/exclude
file may also be helpful.
🧪 git-draft
is currently an experimental prototype.
I’m publishing it to get feedback on the design, and attract interest in improving this aspect of the Git user experience. The data model (see Implementation below) seems pretty good, but the current implementation needs to be completely rewritten to become production-ready. (See the Bugs section at the bottom of this doc for a brief overview of the situation.)
Please try it out and open issues and discussions. Even if it’s just “I don’t understand this at all” that’s still useful feedback to get!
Caveats:
- There are a lot of edge cases for which
git-draft
will fail with mysterious error messages; bug reports gladly accepted. I’ve been trying to catch these as I find them, but the current implementation is not exactly robust. - I can't guarantee it won't eat your data. (I trust it enough to use it on our monorepo at work, and I’ve never had it mangle anything, but caveat emptor nonetheless.)
- Breaking changes will be announced in the release notes, but may come at any time.
I don’t recommend it for beginner git users, yet.
Everybody is already using the staging area so if you run into trouble you can usually find advice to help.
The same is not true of git-draft
, so even though the ‘draft commit’ model is conceptually simpler,
nobody will know how to fix it if you get into a muddle.
git-draft
is distributed as a single large Bash script. After installation, git
will automatically detect it and make git draft
commands work. To install:
- Download the latest version from the releases page.
- Put it somewhere in your
$PATH
, like/usr/local/bin
. - Mark the file as executable with
chmod a+x /path/to/git-draft
. - Test that it’s working by running
git draft help
.
(To un-install, remove this file. git-draft
does not create any other files or settings.)
Note for MacOS users: git-draft
requires Bash 4.2 or greater. MacOS ships with the horribly out of date Bash 3.2 (from 2006!), so you will need to get a newer version of Bash first. If you use homebrew, you can upgrade with:
brew install bash
git-draft
doesn’t need any setup; you can start using it on a repository at any time. (You can also stop using it at any time. The output of git draft status
may be slightly confusing if you ran git commit
while you had a named draft active, but nothing will break.)
If you start with a clear working directory, git draft status
will tell you that you have an “empty draft on branch”.
Make some changes and run git draft status
again. You will see the changed files. Notice that staged and unstaged files are displayed the same way, because git-draft
does not care about this distinction.
Run git draft message
and write a message explaining the changes you made.
Run git draft status
again and notice that it now has the message you just wrote.
When you are ready to commit the changes you’ve drafted, run git draft commit
. (You can also run git draft commit --no-edit
if you don’t need to change the commit message again.) You will now have a new commit, and git draft status
will once again report that your active draft is empty.
Make some changes again, but this time instead of committing them run git draft switch -c
. This will stash your active draft (printing the name assigned to the draft), and give you an empty draft again. (The -c
stands for --create
, because you are creating a new empty draft rather than switching to an existing draft.) Run git draft list
to see your newly created draft.
To switch back to the draft you just stashed, run git draft switch <draft-name>
. If you have a non-empty draft active already, it will be automatically stashed to make way for the draft you are switching to. If the draft was created on a different branch, that branch will be automatically checked out as well. (See the --onto
options to git draft switch
for ways to change this.)
This should be all you need to get started using git draft
in your workflow. For more advanced options, see Commands and Concepts below.
Use git draft help
and git draft <subcommand> --help
to view flags and more options. The major subcommands are:
-
git draft status
: Draft-aware replacement forgit status
(mostly—for implementation reasons it's currently missing some info fromgit status
) -
git draft commit
: Draft-aware replacement forgit commit
: make the active draft into an actual commit. (Understands draft messages and doesn't require staging changes first.) -
git draft message
: Edit the active draft's message. Useful to prepare a commit message before committing. -
git draft list
: View a list of all drafts. -
git draft switch -c
: Stash the active draft and start a new empty draft -
git draft switch <draft-name>
: Stash the active draft (if any) and switch to a stashed draft. By default, also switches to the branch that the draft was created on. -
git draft create
: Create a new (possibly named) stashed draft. -
git draft create -id
: Put staged changes into a new stashed draft, and remove them from the working directory. Useful to split up changes into separate drafts and commits. -
git draft show
: Display a draft (likegit show
). -
git draft drop
: Delete a draft.
(These are the porcelain commands. Plumbing commands are documented throughout the Implementation section below.)
git-draft
introduces the concept of draft commits (usually just called "drafts" for brevity and clarity).
A draft has a parent commit, a commit message, and a tree.
Drafts do not have committer/author dates or a hash.
Notably, a draft's message can be edited at any time, so you can start writing a commit message before you make the commit.
A draft can be in one of two states:
- The active draft is the draft currently being worked on; its tree is the working directory. There is always an active draft, though it may be an empty draft if (a) the draft has not been given a name, (b) the working directory is clean, and (c) the draft has no message.
- Stashed drafts are drafts other than the active draft, which have been put away for later. They are stored inside
.git
and can be activated withgit draft switch
.
Draft names have the same requirements as branch names.
All stashed drafts have unique names.
It is not possible for a single draft to have multiple names.
The active draft may, but is not required to, have a draft name.
If a draft name is needed but not provided (for example, when an unnamed active draft is stashed or when git draft create
is called),
a draft name is autogenerated from the name of HEAD plus a random identifier.
The draft name active
is reserved as a reference to the active draft (and is the only exception to the "multiple names for one draft" rule).
An empty draft is what you get when the active draft has no name, message, or changed files. (Empty drafts only appear as active drafts, since all staged drafts have a name.) If the active draft is an empty draft and you switch to another draft, the empty draft simply disappears.
A blank draft is a draft which has a name, but no message or changed files.
A blank draft may be created by explicitly naming an empty draft, or by git draft create
.
A bare draft is a draft which has a message, but no changed files. A bare draft may be created by adding a message to a blank or empty draft.
When switching to a different draft:
- The current active draft is stashed (if not empty) and the working directory cleaned.
- The selected draft is set as the active draft.
- The selected draft's changes are applied to the working directory.
The active draft is actually an implicit entity, with no reified representation:
- its tree is the working directory
- its parent is HEAD
- its commit message is stored in
.git/GITDASH_DRAFT_COMMIT_EDITMSG
- its name is stored as a trailer called
Draft-ref
in.git/GITDRAFT_NOTES
An "empty active draft" is simply what you get when
- the working tree has no changes, and
.git/GITDASH_DRAFT_COMMIT_EDITMSG
and.git/GITDRAFT_NOTES
do not exist or are zero bytes long
The commands stash
and activate-stashed
convert between the active and staged forms.
(The latter is a plumbing command, which should normally be called via switch
.)
They check that the active draft is empty before making changes to ensure that a bug doesn't cause two drafts to collide.
The plumbing command active-is-empty
implements the check for this state.
Stashed drafts are currently stored as commits under refs/drafts/
.
Their tree/parent/message are stored in this commit, and when the draft is edited the ref is changed to a new commit accordingly.
(As a result, the note in the Concepts section that "drafts do not have committer/author dates or a hash" is not strictly accurate for stashed drafts,
but that should be considered an implementation detail and not relied on.)
Some metadata about the draft is stored as a note on that commit:
in particular, the HEAD at the time they were stashed is stored as Draft-on
, and is used by default to check out the same
branch when switching back to the stashed draft. (This can be changed with the various --onto
options to git draft switch
.)
Despite the name, stashed drafts do not use git stash
, since the structure that produces is somewhat unsuitable.
git stash
entries have two commits, one for the index and one for the working tree, but the former is not needed for drafts;
also, it is not possible to give them stable names and they are instead stored as a "stack" in the reflog of refs/stash
.
"Merge drafts" do not exist (yet?); a draft can only have one parent. For the moment, the parent commit of a draft must be an actual commit, not a draft; if you want to stack them, a branch is a better idea.
git-draft
is completely oblivious to the index. This is by design, but it would be nice to have index-aware commands so that more conventional tools can be used at the same time. (For example, you could interactively add things to the index withgit gui
and then stash only those changes into a draft, leaving the unstaged changes in the active draft.)git draft create --from-index
is a rudimentary version of this.- It probably doesn't interact well with worktrees right now. (It certainly doesn't have a concept of multiple active drafts. I think that the current implementation should largely work, but I have not tried it yet.)
- A lot of basic operations (
get-commit-message
anddescribe
in particular) are quite slow, so the whole thing tends to be kind of sluggish. - Older versions of drafts ought to be retrievable via the reflog, but I haven't investigated exactly how to make this work.
- There's no way to rename a draft once it gets a name, mostly because I haven't needed this.
- In general, there are probably some interesting edge cases with how drafts interact with other parts of
git
. git-draft
is currently implemented as a collection of Bash scripts. This works reasonably well and was an excellent way to get a prototype working, but there are some things that are simply not practical without makinggit-draft
part of thegit
suite rather than a standalone program. For example, commit messages are missing all of the useful notes that Git adds in the editor. (The shell-based implementation also has performance problems, since a lot of forking needs to happen—particularly forgit-draft list
which is noticeably slowish even with only a few drafts.)