It helps doing \show\myvar and \show\testgit. The former would issue
> \myvar=macro:
->.
while the latter would issue
> \testgit=macro:
->\par .
so you see that \testgit is not empty.
By the way, you also get
|gitrev-parseHEADsh: gitrev-parseHEAD: command not found
and you need
\sys_get_shell:nnNTF {git~rev-parse~HEAD}
However the problem is that when you set \myvar you're under \ExplSyntaxOn, but not when you set \testgit. In the latter case, the system returns nothing, which generates an empty line. The \input| mechanism that's exploited by \sys_get_shell:nnNTF actually generates a pseudofile and TeX always appends another EOL at the end. So you get an empty line according to TeX tokenization rules and this is tokenized as \par.
Solution:
\ProvidesClass{test}
\ExplSyntaxOn
\keys_define:nn { test }{
git .code = {
\sys_get_shell:nnNTF {git~rev-parse~HEAD} {\endlinechar=-1~} \testgit {} {}
}
}
\sys_get_shell:nnNTF {git~rev-parse~HEAD} {} \myvar {} {}
\ExplSyntaxOff
\ProcessKeyOptions[test]
\LoadClass{article}
New test.tex:
\documentclass[git]{test}
\usepackage[T1]{fontenc}
\begin{document}
|\verb|\myvar = |\texttt{\meaning\myvar}|
|\verb|\testgit = |\texttt{\meaning\testgit}|
|\ExpandArgs{e}\IfBlankTF{\myvar}{}{a}|
|\ExpandArgs{e}\IfBlankTF{\testgit}{}{b}|
\end{document}

Document Class: test (|gitrev-parseHEADsh: line 1: gitrev-parseHEAD: command not found ) (|gitrev-parseHEADsh: line 1: gitrev-parseHEAD: command not foundyou need e.g.git ~ rev-parse ~ HEAD. so i don't know how you get a difference between the two cases?\testgitis correctly set even with a.gitdirectory. I think the reason is that, as egreg mentions in the answer or Ulrike mentions in the above comment,\testgitis not set inexpl3syntax.\keys_define:nnthe key property is named.code:n(.codeis from the 2e layer, though internally they're the same).