rcmdnk's blog

20240108_gitls_200_200

git ls-filesはディレクトリを指定してもそのディレクトリ以下のすべての管理されているファイルを表示します。

シェルで使うlsの様にカレントディレクトリだけを表示したい時はちょっと工夫がいります。

また、ignoreされているファイルやファイルの状態も見たい、ということで それを実現するaliasについて。

現在のディレクトリで管理されているファイルのみを表示

how to git ls-files for just one directory level. - Stack Overflow

上にある答えを使って

1
$ git ls-files ':(glob)*'

とすれば現在のディレクトリで管理されているファイルのみを表示できます。

一番簡易にはこれで足りることが多いかと思います。

変更したあとに細かく見ようと思うとこれだとチョット足りません。

ls-filesには-tといった状態を示すオプションもありますが、 これは引数の状態に合わせて表示されるだけで、 通常のls-filesであれば全てH(tracked file that is not either unmerged or skip-worktree)になります。

変更が入っているファイルだけを表示したければ git ls-files -m -tとかするとCと表示され、変更されたものでstagingされてないものだけが表示されます。

ただ、stagedな状態でmodifiedとかの状態はわからないので全て何もしてないファイルと同等に見えてしまいます。

管理されてるディレクトリのみ表示

How to git list only the tracked directories? - Stack Overflow

上の最初の答えでは

1
$ git ls-files | xargs -n 1 dirname | uniq

となってますが、これだとdirnameなので下に多階層のディレクトリ構造があると上手くいきません。

直接cut -d '/' -f1とかで良さそう。

そのままやるとカレントディレクトリのファイルも/を含まないですがそのまま表示されてしまうので

1
$ git ls-files ':(glob)*/**' | cut -d '/' -f1 | uniq

とすれば必要なものが取得できます。

加えて、各ディレクトリの中に変更があるかどうかも見たい時、git statusの結果を見て

1
2
3
4
5
6
7
8
9
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:    a

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   b

のように変更があるとstagedunstagedでそれぞれで、

  • staged: Changes to be committed:
  • unstaged: Changes not staged for commit:

といった出力があります。

これを各ディレクトリ毎に見て、 以下で見るgit status -sの表記と同じになるように

1
2
3
4
5
git ls-files ':(glob)*/**' | cut -d '/' -f1 | uniq | while read -r line;do
  staged=$(git status $line|grep -q "Changes to be committed:\" && echo "M" || echo " ")
  unstaged=$(git status $line|grep -q "Changes not staged for commit:" && echo "M" || echo " ")
  echo "$staged$unstaged $line/"
done

と、それぞれ何らか変更があればM、なければスペースを表示するようにできます。

最近の通常のシェルスクリプトなら

1
2
3
while read -r line;do
  ...
done < <(git ls_files ...)

のように書くことも可能で、こちらのほうがサブシェルに入らないので色々便利ですが、 この書き方はgitの中のシェルが理解してくれないのでaliasを書く際には上のようなパイプで繋いだ形でやる必要があります。

変更されたファイル、ignoreされたファイルの表示

git status -sで現在のレポジトリ内の変更があったファイルに関して、staged, unstaged両方の状態を簡単に見ることができます。

1
2
3
$ git status -s
M  a
 M b

とかならaは変更された後git addされている状態、bは変更されたけどgit addされていない状態です。

また、git mvrenameな状態にあると、

1
2
$ git status -s
R  a -> b

みたいな感じで表示されます。

さらに--ignoredオプションを加えると.gitignoreで指定されている除外ファイルも

1
!! x

のように!!な状態として表示されます。

このgit stagusでもls-files同様に:(glob)*を渡すことで現在のディレクトリのみを見ることが可能です。

ただし、1つだけとれないのが別のディレクトリからrenameされてきたもの。

cというファイルが他のディレクトリからgit mvで持ってこられるとそのままだと

1
2
$ git status -s
R  other/c -> c

の様にrenameしたことがわかりますが、元のファイルのディレクトリを除いた範囲で見てみると

1
2
$ git status -s
A  c

と、単に新しく加えられたファイルのように見えてしまいます。

このrenameもきちんと捉えるためと、カレントディレクトリ中でignore、及びまだ追加されていないディレクトリ も含めて取得するために以下のようなスクリプトを考えます。

1
2
3
4
5
6
7
8
9
10
11
12
git status -s --ignored | while IFS= read -r line;do
  file=$(echo $line | cut -d ' ' -f2)
  renamed=$(echo $line | cut -d ' ' -f4)
  if echo $file | grep -q "^\.\." &&  ([ -z "$renamed" ] || echo $renamed | grep -q "^\.\.");then
    continue
  fi
  if ! echo $file | grep -q -E "/.+";then
    echo "$line"
  elif [ -n "$renamed" ]&& ! echo $renamed | grep -q "/";then
    echo "$line"
  fi
done

こんな感じに、git statusでは全て表示させ、2列目とrenameがあった場合の4列目 を見てそれが/を含まない、つまりカレントディレクトリのものであれば見るようなことができます。

while IFS=と区切り文字を変更しているのは出力の最初にスペースがある可能性があるので、 元のIFSのままだとそこが消されてしまうのでそれを防ぐためにIFS=として区切り文字をなくすことで 行ごとにそのまま扱うことができます。

また、各行でもgit status -sだと最初にスペースがあるかないか、1列目と2列目で区切りがスペース1個か2個か分かれますが、 echo $lineでquoteしないと最初のスペースは消され、文字列中のスペースが複数あっても全て1つにまとまるので 後ろで簡単にcutで区切れます。

スクリプトではまずstatusを表示されたファイル(及びディレクトリ)とそれがrenameされたものの場合にはrename先の名前を取得します。

最初のif文はそのどちらもがカレントディレクトリよい上のディレクトリにある場合にskipするもの(../でスタートするものを除く)。

次のif文ではスラッシュが含まれないカレントディレクトリのファイル、及び最後がスラッシュになっているカレントディレクトリにあるディレクトリのみを抜き出して表示しています。

これに引っかからなかった時、rename先のファイルがあり、それがカレントディレクトリのファイルであれば表示しています。

空のディレクトリの検出

空のディレクトリはgitの管理下にはなりませんが、ignoreされてるわけでもunstagedな状態にあるわけでもありません。

なので上の作業では一切出てきませんが、とりあえずカレントディレクトリにあるディレクトリをパット表示するには

1
file * |grep " directory$"|cut -d ":" -f1|grep -v "^.git$"

こんな感じで。

.gitディレクトリだけは手動で外します。

aliasにまとめる

ファイルを表示する git ls-files ':(glob)*'には変更があってgit statusに表示されるものも含まれるので、 まずgit statusから一覧を作り、その中にないものでls-filesで見つかったファイルを追加します。

git statusではディレクトリはignoreされているもの、まだ追加されていないもののみが表示されるため、 git ls-filesでは表示されません。 したがって上で作ったstatus込のディレクトリはそのまま追加します。

これを.gitconfig[alias]欄に追加出来る様に書き直すと

.gitconfig
1
2
3
4
5
6
7
8
9
[alias][
  ls-here = "!f () {\
    cd ./${GIT_PREFIX};\
    files=\"$(git status -s --ignored | while IFS= read -r line;do file=$(echo $line | cut -d ' ' -f2);renamed=$(echo $line | cut -d ' ' -f4);if echo $file | grep -q \"^\\.\\.\" &&  ([ -z \"$renamed\" ] || echo $renamed | grep -q \"^\\.\\.\");then continue;fi;if ! echo $file | grep -q -E \"/.+\";then echo \"$line\";elif [ -n \"$renamed\" ]&& ! echo $renamed | grep -q \"/\";then echo \"$line\";fi;done)\";\
    files=\"$(echo \"$files\"; git ls-files ':(glob)*' | while read -r line;do if ! echo \"$files\" | awk '{print $2}' | grep -q \"^$line$\" && ! echo \"$files\" | awk '{print $4}' | grep -q \"^$line$\";then echo \"   $line\";fi;done)\";\
    files=\"$(echo \"$files\"; git ls-files ':(glob)*/**' | cut -d'/' -f1 | uniq | while read -r line;do staged=$(git status $line|grep -q \"Changes to be committed:\" && echo \"M\" || echo \" \");unstaged=$(git status $line|grep -q \"Changes not staged for commit:\" && echo \"M\" || echo \" \");echo \"$staged$unstaged $line/\";done)\";\
    files=\"$(echo \"$files\"; ls -a | while read -r line;do if [ ! -d \"$line\" ] || [ \"$line\" = \".\" ] || [ \"$line\" = \"..\" ] || [ \"$line\" = \".git\" ];then continue;fi;if ! echo \"$files\" | cut -c 4- | awk '{print $1}' | grep -q \"^$line/$\";then echo \"-- $line/\";fi;done)\";\
    echo \"$files\" | sort -k1.4;\
  };f"

こんな感じに。 while文はパイプを使う形にしているので中で変数を変更しても後で使えないため、echoとかの出力を外側で変数に入れる必要がありますが、 これをするためにワンライナーにして$()の中に突っ込んで、filesという変数の中に入れています。

最初にgit statusの結果。

次にgit ls-filesでカレントディレクトリのファイルを追加。 ここではfilesをまず表示して、その中に無いものだけを選ぶようにfilesの出力からawk '{print $2}、及びawk '{print $4}'で ファイル部分、rename先を取得してそれらとの比較などを行って重複を除いています。

次にgit ls-filesを使ってカレントディレクトリないの管理されているディレクトリの追加。

最後に空のディレクトリの追加。ここではgit ls-filesの結果で最初のstatusが全部空のものもあるので直接awkではなくcut -c 4- | print '{print $1}'で文字位置を見て抜き出すようなことをしています。

一番最後はファイル名の部分でソートして出力です。(2文字のstatus, 1文字のスペースがあるので、1番目のfield位置、つまり先頭から4文字目以降でソートする。)

もうちょっときれいな書き方も出来るかもしれませんがとりあえずこれで、

1
$ git ls-here

とすることで現在のディレクトリのファイルの状態をぱっと見ることができます。

以下のようなテストレポジトリで見てみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ ls
a  b  c  d  h  l  m  o  t  u  v  w  y  z
$ echo $(git ls)
.gitignore a c d f h l o v/v z/g z/i z/j z/k
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   c
        deleted:    e
        renamed:    z/h -> h
        new file:   l
        renamed:    n -> o
        renamed:    g -> z/g

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   d
        deleted:    f
        modified:   z/i

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        m
        u/
        y/

git ls-hereすると

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ git ls-here
   a
!! b
M  c
 M d
D  e
 D f
   .gitignore
R  g -> z/g
A  l
?? m
R  n -> o
-- t/
?? u/
   v/
!! w/
?? y/
MM z/
R  z/h -> h
Sponsored Links
Sponsored Links

« ObsidianでEvernoteライクにWebクリップ、トレイアイコン化で使う GitHub Actionsでのsyntax error near unexpected token `(' 回避 »

}