諭吉佳作/men

この1か月くらい、家では諭吉佳作/menさんの曲を聴きまくっている。それくらいハマっている。

最初に諭吉佳作/menさんを知ったのは、9月にNHK Eテレで放送された「前山田×体育のワンルーム☆ミュージック」だった。インタビューで、「打ち込みだったら腕が3本なければ叩けないような曲も作れる」(もっと違うニュアンスだったかも)ってとこが印象に残っている。

この時点では、名前は知っている(名前の印象はとても強い)くらいでしかない状態で、特に曲を聴いてみようとはならなかった。この時本人が歌っている曲が流れていたら、きっとこの時点ですぐにハマっていたと思う。
このとき番組で流れていたのは楽曲提供していたでんぱ組.incのMVだった。

そのあと、SpotifyでWeekly Buzzか何か聞いているときに、崎山蒼志くんの「むげん・ (with 諭吉佳作/men)」が流れて、この声って諭吉佳作/menさん!?ってなった。
Spotifyにはほとんど曲がなかったので、YouTube、SoundCloudで曲を聴いた。すごくいい。

曲自体も良いのだけど、そこに声が重なってさらに魅力的なものになっている。この曲とこの声が組み合わさることによる相乗効果というかなんというか。
どれもオリジナリティあふれる楽曲で、ビリー・アイリッシュを初めて聞いた時の感覚に似てる。(ビリー・アイリッシュの曲を初めて聞いた時も、とてもインパクト受けた)

一番のお気に入りは「洋装の語る今日は」で、リズミカルに流れる曲の上でさらに流れるような声、、すごくいい。

soundcloud.com

ちなみに、うちの娘たちは「の ぞ き」がお気に入りで、よく口ずさむようになった。

移動中などにも聞きたいので、Spotifyで配信(課金してるので、ネット接続なくても聴ける)するか、アルバム出して欲しいです。もしくはダウンロード販売でも。
同じように思っている人は既にいっぱいいそうなので、すぐ近い将来そうなるんだろうな。とても楽しみ。

プロジェクト切り替え時にウォッチャーを変更する(Redmine View Customize Plugin)

プロジェクト切り替え時にウォッチャーを変えたいのだけどうまくいかないといった問い合わせもらったので、サンプルコード書いてみました。

入力フォーム差し替えと絡んでハマりそうな箇所なので、他の方の参考にもなればと思います。

入力フォームの差し替え

プロジェクトやステータス、トラッカーを切り替えた際には、入力フォーム自体が差し替えられます。サーバ側を呼び出して、受け取ったHTMLの断片で差し替えるようなイメージです。

チケットの題名や説明、カスタムフィールドなどは、空の入力フォームを作った後に、もともと設定されていた情報に移し替えています。
ただ、ウォッチャーのところは、チェックの付与情報自体が付いたHTMLがサーバ側からかえってきて、それをそのまま差し替えるだけです。(サーバ呼び出しの際にフォームの情報も送っている)
そのため、サーバの呼び出しが行われた後に画面側の情報を変えてもそれが反映されません。

ということで、サーバ側呼び出しの前に画面側を変えてあげる必要がありますが、そうなるとイベントのキャプチャフェーズで拾ってあげる必要があります。jQueryではキャプチャフェーズを指定できないので、addEventListenerで指定する形となります。

設定内容

  • Path pattern: .*
  • Insertion position: Bottom of issue form
/*
Path pattern: .*
Insertion position: Bottom of issue form(チケット入力欄の下)
Type: JavaScript

プロジェクトを変更したら、ウォッチャーを変更する
*/
$(function() {

  var changeWatcher = function() {
    switch($('#issue_project_id').val()) {
      case '1':
        $('#issue_watcher_user_ids_1 input').prop('checked', true);
        break;

      case '2':
        $('#issue_watcher_user_ids_8 input').prop('checked', true);
        break;

      case '3':
        $('#issue_watcher_user_ids_9 input').prop('checked', true);
        break;
    }
  };

  document.getElementById('all_attributes').addEventListener(
    'change',
    function(e) {
      if (e.srcElement.id == 'issue_project_id') {
        // プロジェクト変更時のみ
        changeWatcher();
      }
    },
    true); // キャプチャフェーズ
})

gitlab-project-member-exporter - GitLabからプロジェクト毎のメンバー一覧をエクスポートするツールを作った

この記事はGitLab Advent Calendar 2019の2日目の記事です。

GitLabからプロジェクト毎のメンバー一覧をエクスポートするツール、gitlab-project-member-exporter を作りました。

なぜ作ったか

GitLabでグループ、メンバーが多くなってくると、誰がどこにアクセス可能な状態なのかの把握が面倒になってきます。

適当なタイミングで確認しようとしても、プロジェクトやユーザの情報をGitLabの管理画面から見ていくしかありません。数プロジェクトならばどうにかなりそうですが、数十、数百となってくると、ページ辿るだけでも面倒な作業になります。

全プロジェクト×メンバの一覧ができてしまえば、確認が少しは楽になるだろうということで、今回ツールとして作りました。

できること

このツールを使うと、下記のようなCSVファイルをエクスポートできます。プロジェクトとそれに所属するメンバーの一覧になります。

実際の権限と同様に、グループに権限があった場合、その配下のプロジェクトに権限があるものとして一覧化しています。

プロジェクトメンバ一覧

Project: Name Project: Visibility User: Name User: Username User: State Permission
Administrator / my-project private Administrator root active Maintainer
group1 aaa / project-b private Administrator root active Owner
group1 aaa / project-b private user2 user2 active Developer
group1 aaa / project-a private Administrator root active Owner
group1 aaa / project-a private user2 user2 active Developer
group1 aaa / project-a private user1 user1 blocked Maintainer

また、ユーザ一覧、プロジェクト一覧もエクスポートできます。

ユーザ一覧

Name Username Email State Admin Created at Last sign in at Last activity on
user3 user3 user3@example.com active FALSE 2019-11-16T13:37:15.885Z
user2 user2 user2@example.com active FALSE 2019-11-16T13:36:29.651Z
user1 user1 user1@example.com blocked FALSE 2019-11-16T13:35:44.406Z
Administrator root admin@example.com active TRUE 2019-11-16T07:50:47.821Z 2019-11-24T14:50:54.996Z 2019-11-25

プロジェクト一覧

Name Visibility Created at Last activity at
Administrator / my-project private 2019-11-16T13:48:47.727Z 2019-11-16T13:48:47.727Z
group1 / project-b private 2019-11-16T13:48:11.161Z 2019-11-24T15:08:11.024Z
group1 / project-a private 2019-11-16T13:44:20.849Z 2019-11-16T13:44:20.849Z

これら情報を使うことによって、いちいちGitLabのページを辿らなくても、プロジェクトメンバーの状況が一覧で確認できます。
新しいユーザやプロジェクトが追加されたこともわかるので、それらに対してどのように権限が設定されているのか、、といったことも追うことができます。

定期的に出力して、差分を見るというのも良いと思います。

技術的なところ

GitLabのAPIを利用しています。

必要なリソースへアクセスするためのAPIは揃っており、特に悩むことはありませんでした。 (Paginationの情報がレスポンスのヘッダに乗ってくることを見逃していたくらい)

使っているAPIは下記でまとめています。

利用方法

利用方法については、下記READMEをご参照ください。

Javaで実装されています。必要な情報に過不足があるようでしたら、カスタマイズしてご利用ください。

GitLabのAPIを利用して、ユーザが参照できるプロジェクト一覧を作成する

GitLab上で、誰がどのプロジェクトを参照可能かを一覧化したかったので、GitLabのAPIを利用して一覧化してみます。

GitLab Community Editionを利用して確認しました。

GItLabのAPI

APIの情報は下記ドキュメントにまとまっています。

認証方法はいくつかありますが、今回はPersonal access tokenを利用してアクセスしてみます。Personal access tokenはUser Settingsから作成可能です。

Scopeはapiにしました。

グループメンバーの取得

プロジェクトを参照できるのは、下記の2パターンとなります。

  1. 上位のグループのメンバー
  2. プロジェクトのメンバー

そのため、単にプロジェクトのメンバーの情報だけでアクセス可否が決まるのではなく、グループのメンバーの情報も考慮する必要があります。

ということで、まずはAPIでグループ一覧を取得します。

GET http://gitlab.example.com/api/v4/groups?access_token=<access token>

下記のようにグループ一覧が取得できました。

[
  {
    "id": 5,
    "web_url": "http://gitlab.example.com/groups/group1",
    "name": "group1",
    "path": "group1",
    "description": "",
    "visibility": "private",
    "share_with_group_lock": false,
    "require_two_factor_authentication": false,
    "two_factor_grace_period": 48,
    "project_creation_level": "developer",
    "auto_devops_enabled": null,
    "subgroup_creation_level": "maintainer",
    "emails_disabled": null,
    "lfs_enabled": true,
    "avatar_url": null,
    "request_access_enabled": true,
    "full_name": "group1",
    "full_path": "group1",
    "parent_id": null
  },
  {
    "id": 6,
    "web_url": "http://gitlab.example.com/groups/group2",
    "name": "group2",
    "path": "group2",
    "description": "",
    "visibility": "private",
    "share_with_group_lock": false,
    "require_two_factor_authentication": false,
    "two_factor_grace_period": 48,
    "project_creation_level": "developer",
    "auto_devops_enabled": null,
    "subgroup_creation_level": "maintainer",
    "emails_disabled": null,
    "lfs_enabled": true,
    "avatar_url": null,
    "request_access_enabled": true,
    "full_name": "group2",
    "full_path": "group2",
    "parent_id": null
  }
]

ここからさらにグループのメンバーを取得します。

GET http://gitlab.example.com/api/v4/groups/5/members?access_token=<access token>
[
  {
    "id": 1,
    "name": "Administrator",
    "username": "root",
    "state": "active",
    "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
    "web_url": "http://gitlab.example.com/root",
    "access_level": 50,
    "expires_at": null
  },
  {
    "id": 3,
    "name": "user2",
    "username": "user2",
    "state": "active",
    "avatar_url": "https://www.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?s=80&d=identicon",
    "web_url": "http://gitlab.example.com/user2",
    "access_level": 30,
    "expires_at": null
  }
]

これを繰り返し取得することによって、各グループのメンバー一覧が作成できます。

プロジェクトメンバーの取得

プロジェクト一覧を取得します。なお、グループから辿っていく方法もありますが、ユーザが持つプロジェクトも含めたかったので、プロジェクト一覧から辿るようにしました。

GET http://gitlab.example.com/api/v4/projects?simple=true&access_token=<access token>

プロジェクト一覧は下記のようになります。

[
  {
    "id": 3,
    "description": "",
    "name": "my-project",
    "name_with_namespace": "Administrator / my-project",
    "path": "my-project",
    "path_with_namespace": "root/my-project",
    "created_at": "2019-11-16T13:48:47.727Z",
    "default_branch": null,
    "tag_list": [],
    "ssh_url_to_repo": "git@gitlab.example.com:root/my-project.git",
    "http_url_to_repo": "http://gitlab.example.com/root/my-project.git",
    "web_url": "http://gitlab.example.com/root/my-project",
    "readme_url": null,
    "avatar_url": null,
    "star_count": 0,
    "forks_count": 0,
    "last_activity_at": "2019-11-16T13:48:47.727Z",
    "namespace": {
      "id": 1,
      "name": "Administrator",
      "path": "root",
      "kind": "user",
      "full_path": "root",
      "parent_id": null,
      "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
      "web_url": "http://gitlab.example.com/root"
    }
  },
  {
    "id": 2,
    "description": "",
    "name": "project-b",
    "name_with_namespace": "group1 / project-b",
    "path": "project-b",
    "path_with_namespace": "group1/project-b",
    "created_at": "2019-11-16T13:48:11.161Z",
    "default_branch": null,
    "tag_list": [],
    "ssh_url_to_repo": "git@gitlab.example.com:group1/project-b.git",
    "http_url_to_repo": "http://gitlab.example.com/group1/project-b.git",
    "web_url": "http://gitlab.example.com/group1/project-b",
    "readme_url": null,
    "avatar_url": null,
    "star_count": 0,
    "forks_count": 0,
    "last_activity_at": "2019-11-16T13:48:11.161Z",
    "namespace": {
      "id": 5,
      "name": "group1",
      "path": "group1",
      "kind": "group",
      "full_path": "group1",
      "parent_id": null,
      "avatar_url": null,
      "web_url": "http://gitlab.example.com/groups/group1"
    }
  },
  {
    "id": 1,
    "description": "",
    "name": "project-a",
    "name_with_namespace": "group1 / project-a",
    "path": "project-a",
    "path_with_namespace": "group1/project-a",
    "created_at": "2019-11-16T13:44:20.849Z",
    "default_branch": null,
    "tag_list": [],
    "ssh_url_to_repo": "git@gitlab.example.com:group1/project-a.git",
    "http_url_to_repo": "http://gitlab.example.com/group1/project-a.git",
    "web_url": "http://gitlab.example.com/group1/project-a",
    "readme_url": null,
    "avatar_url": null,
    "star_count": 0,
    "forks_count": 0,
    "last_activity_at": "2019-11-16T13:44:20.849Z",
    "namespace": {
      "id": 5,
      "name": "group1",
      "path": "group1",
      "kind": "group",
      "full_path": "group1",
      "parent_id": null,
      "avatar_url": null,
      "web_url": "http://gitlab.example.com/groups/group1"
    }
  }
]

ここからさらにプロジェクトのメンバーを取得します。

GET http://gitlab.example.com/api/v4/projects/1/members?access_token=<access token>
[
  {
    "id": 2,
    "name": "user1",
    "username": "user1",
    "state": "active",
    "avatar_url": "https://www.gravatar.com/avatar/111d68d06e2d317b5a59c2c6c5bad808?s=80&d=identicon",
    "web_url": "http://gitlab.example.com/user1",
    "access_level": 40,
    "expires_at": null
  }
]

この情報に、さらに上位のグループのメンバーの情報を付与することによって、プロジェクトにアクセス可能なユーザ一覧を作ることができます。

おわりに

今回調べた情報を元にツールを作成予定です。作成出来たら追記します。

ShortcutKey2URLでショートカット一覧のエクスポート、インポートに対応しました

要望のあったショートカット一覧のエクスポート、インポートに対応しました。

Firefox版(v4.3.0)、Chrome版(v1.3.0)ともに対応しています。(Chrome版は、この記事を書いているタイミングでは審査待ちです)

Firefox版、Chrome版ともにフォーマットは同じですので、相互で移行が可能になりました。

f:id:onozaty:20191028214240p:plain

redmine-issue-loader のバージョン2.2.0をリリースしました

CSVを読み込んでRedmineのチケットを新規作成、更新するツール、redmine-issue-loaderのバージョン2.2.0をリリースしました。

Basic認証に対応しています。(APIアクセスキーを使うか、Basic認証のどちらかを選択可能に)

JavaScriptでファイルのインポート、エクスポートを実装する

Chrome/Firefoxの拡張機能で、設定をインポート、エクスポートする機能が欲しいといった要望があったので、まずはどんな感じにインポート、エクスポートすれば良いんだっけ、、ってことでJavaScriptで簡単なサンプル書いてみました。

コード

下記のようなHTMLを用意します。インポート、エクスポート用のボタンがあって、インポートしたものを表示するエリアを用意しておきます。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <button type="button" id="inputFile">Import</button>
    <pre><code id="contents"></code></pre>
    <button type="button" id="outputFile">Export</button>
    <script src="file.js"></script>
  </body>
</html>

JavaScriptは下記のような感じで。

let currentContents = '';
const displayElement = document.getElementById('contents');

document.getElementById('inputFile').addEventListener('click', (e) => {
  const fileInput = document.createElement('input');
  fileInput.type = 'file';
  fileInput.setAttribute('hidden', true);

  fileInput.addEventListener('change', (e) => {
    const file = e.target.files[0];
  
    const reader = new FileReader();
    reader.onload = (e) => {
      const fileContents = e.target.result;
      displayElement.textContent = fileContents;
      currentContents = fileContents;
    }
    reader.readAsText(file);
  }, false);
  
  document.body.appendChild(fileInput);
  fileInput.click();
  fileInput.remove();
}, false);


document.getElementById('outputFile').addEventListener('click', (e) => {
  const downloadLink = document.createElement('a');
  downloadLink.download = 'export.txt';
  downloadLink.href = URL.createObjectURL(new Blob([currentContents], { 'type' : 'text/plain' }));
  downloadLink.setAttribute('hidden', true);

  document.body.appendChild(downloadLink);
  downloadLink.click();
  downloadLink.remove();
}, false);

インポートのボタンが押下された際に、ファイル選択用のinput要素を生成してclickを呼び出してファイル選択ダイアログを表示させます。
ファイルが選択されたら、それをFileReaderを使って読みだして表示します。

エクスポートのボタンが押下されたら、BlobからURLを生成し、それをリンクとしてa要素を生成してclickといった流れでダウンロードさせています。

Firefoxだとbody配下に入れないとclickが効かないので、body.appendChildしてremoveといったことをやっています。

動作

とりあえずFirefox、Chrome、Edgeの最新バージョンで動作することを確認済みです。

f:id:onozaty:20191014010546g:plain