Wandering Thoughts archives

2016-09-05

Using Magit to selectively discard changes in your git working tree

Suppose, not entirely hypothetically, that you have been hacking around with something that uses git and you've wound up with some code changes in your working tree that have definitely turned out to not be what you want. If these changes are the only changes you've made to a particular file, you can restore the file with 'git checkout'. If all of the other changes to the file are ones that are ready to commit, you can use Magit to selectively stage them, commit what you've staged, and now that the file only has unwanted changes you can use 'git checkout' on it.

But suppose neither of these are true. The file has some other changes in it, but you're not ready to commit them. Wouldn't it be nice if you could use magit to selectively discard changes in a file, in the same sort of nice interface as selectively staging chunks? The answer is that you can, but if you are me, how you do this is not necessarily obvious from the magit manual.

When I wanted to do this recently, I searched through the manual's table of contents for something that seemed applicable and didn't find anything. Reverting isn't it (that involves actual commits), and resetting is a whole tree activity. The answer is that magit puts this in a variant of staging and unstaging changes and does it out of the magit status buffer, which makes a kind of sense; discarding changes from the working tree is sort of the strong version of unstaging them. In specific, I want the k operation (aka magit-discard). This works just like staging and unstaging does, which means that you can apply it to an explicitly selected change within a chunk as well as to a chunk or an entire file.

There's some other interesting things in magit's Applying section. If I'm reading and testing things correctly, magit's v operation (aka magit-reverse) provides a mechanism to pull back old, now-deleted things; you'd go to the commit that removed whatever it is, select the chunk or the lines you wanted, then reverse it into your tree (restoring the old version). It's possible that there's a simpler way to do this, of course.

(Well, there's visiting blobs and then just copying chunks from them, but that requires finding the right revision. I may be missing a Magit log buffer feature that visits a file as of the specific commit you're looking at.)

(This is one of those entries that I write for myself, so I hopefully don't have to go through finding this magit feature again the next time I want it.)

programming/MagitDiscardingChanges written at 23:53:22; Add Comment

An argparse limitation with nargs="*" and choices=...

Suppose that you are writing a command where the user can specify one or more additional things for the command to do, as command line arguments. The basic command might be invoked as 'dothing', but you can also run it as 'dothing also-a also-b' (and variants). There are a limited set of valid additional things to do, and trying to do anything else is an error.

On the surface, it looks like argparse can handle this:

parser.add_argument("cmds", nargs="*",
                    choices=("also-a", "also-b", "also-c"),
                    help="Additional things to do")

If you run this with additional arguments, all will seem well. Your allowed additional arguments will be accepted, and any other additional arguments will be rejected with an appropriately specific error message. Then perhaps you will try to run this without any additional arguments:

usage: ....
dothing: error: argument cmds: invalid choice: [] (choose from 'also-a', 'also-b', 'also-c')

I will skip to the conclusion: I've been unable to come up with a good way to fix this. If you need choices=, argparse effectively turns nargs="*" into nargs="+". Otherwise, you can't specify a choices= and must do the validation yourself.

(This issue happens in both Python 2 and Python 3 in my testing. I first ran into it in Python 2, but have seen it in Python 3 as well.)

It's possible to sort of work around this by setting a 'default=' value, but it seems that the default value you need here is a bit non-functional too. Since we've set nargs="*", the proper end value of parser.cmds is a list (and this is what you get if one or more explicit arguments is provided), so you'd expect that you want to set default to a list. If you do that, it is rejected as not matching your choices selection. If you set default to a single value in your choices, eg 'default="also-c", it's accepted but parser.cmds isn't a list if the default is used; instead of being '["also-c"]', it is plain '"also-c"'. You can make your code detect this situation and even use it to tell if an argument was provided explicitly, but the whole thing feels like a fragile house of cards that's going to come tumbling down some day.

(If I was energetic I would try to file this as a Python bug. I may be sufficiently energetic to do this some day, but not right now.)

python/ArgparseNargsChoicesLimitation written at 00:51:26; Add Comment


Page tools: See As Normal.
Search:
Login: Password:
Atom Syndication: Recent Pages, Recent Comments.

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.