CodeCommit APIのCreateCommitの動きが想定と違った話

# 概要

Lambda(python)で作成したファイルを、CodeCommit API(CreateCommit)を使ってリポジトリに追加(更新)しようとしたが上手くいかず、調べたらAPIの仕様が自分の想定と違ったので、備忘録としてその時の内容をまとめる。

# やりたかったこと

S3にあるファイルをLambdaで編集して、編集したファイルをCodeCommitリポジトリに追加(更新)する。

## 作成したLambda関数

```
s3 = boto3.resource('s3')
cc = boto3.client('codecommit')

def lambda_handler(event, context):
    bucket = s3.Bucket('<バケット名>')
    obj = bucket.Object('<オブジェクト名>')
    res_obj = obj.get()    
    body= res_obj['Body'].read()

    〜〜〜（編集処理）〜〜〜

    res_branch = cc.get_branch(repositoryName='<リポジトリ名>',branchName='<ブランチ名>')
    commit_id = res_branch['branch']['commitId']
    
    for (root, folders, files) in os.walk('/tmp/'):
        for file in files:
            file_path = os.path.join(root, file)
            putfile_entry = {'filePath': str(file_path).replace('/tmp/', 'piyo/'),
                            'sourceFile': {'filePath': file_path}}
            putfile_list.append(putfile_entry)
    
    return cc.create_commit(repositoryName='<リポジトリ名>', branchName='<ブランチ名>', parentCommitId=commit_id, putFiles=putfile_list)
```

### この時想定した動作

`putFiles`->`sourceFile`->`filePath`に指定したローカルファイル（この場合はLambda上のファイル）を`putFiles`->`filePath`に指定したリポジトリ内のパスに追加すると思っていた。

## 実行結果

```
[ERROR] FileDoesNotExistException: An error occurred (FileDoesNotExistException) when calling the CreateCommit operation: The file cannot be copied or moved because no file with that name exists in the specified path. Make sure that the file name is correct, and verify whether the file still exists. The file might have been moved or deleted by a previous commit. Source file path: tmp/piyo01.json
Traceback (most recent call last):
  File "/var/task/lambda_function.py", line 102, in lambda_handler
    return cc.create_commit(repositoryName=repo_name, branchName=branch_name, parentCommitId=commit_id, putFiles=putfile_list)
  File "/var/runtime/botocore/client.py", line 357, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/var/runtime/botocore/client.py", line 676, in _make_api_call
    raise error_class(parsed_response, operation_name)END RequestId: e8c03049-767c-41e8-9d3a-833f1d820a5c
```

そんなソースファイルは存在しないとエラーに。。。

## 調査（１）

`create_commit`に指定したパラメーターを確認する。

`create_commit`の`putFiles`に指定した値は以下の通り。

```
[{'filePath': 'piyo/piyo01.json', 'sourceFile': {'filePath': '/tmp/piyo01.json'}}]
```

Lambdaの/tmpディレクトリに出力したファイルがソースファイルに指定できているのに、`FileDoesNotExistException`が発生した？

エラーメッセージをよく見ると、ソースファイルのパスの先頭のスラッシュ(/)がなくなっている。なぜ？

```
Source file path: tmp/piyo01.json
```

なぜ？

## 調査（２）

`create_commit`のドキュメントを確認する。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit.html#CodeCommit.Client.create_commit

### putFilesのパラメータ説明

![スクリーンショット 2021-07-14 14.55.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1698899/d95444e1-debe-8592-690b-3d7f65261014.png)

（sourceFileの説明をGoogle翻訳）

![スクリーンショット 2021-07-14 14.56.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1698899/7563060a-fee9-3637-27b5-a69559842775.png)

ざっくりとした説明でよくわからん。。。

## 調査（３）

CodeCommitのドキュメントを確認する。

https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/how-to-create-commit.html#how-to-create-commit-cli

>file1.py および file2.py という名前のファイルに変更を行うコミットを作成するには、ファイルの名前を picture.png から image1.png に変更し、それを pictures という名前のディレクトリから images という名前のディレクトリに移動して、最新コミットの ID が 4c925148EXAMPLE である MyFeatureBranch という名前のブランチの MyDemoRepo という名前のリポジトリにある ExampleSolution.py という名前のファイルを削除します。

```
aws codecommit create-commit --repository-name MyDemoRepo --branch-name MyFeatureBranch --parent-commit-id 4c925148EXAMPLE --name "Saanvi Sarkar"
 --email "saanvi_sarkar@example.com" --commit-message "I'm creating this commit to update a variable name in a number of files."
 --keep-empty-folders false  --put-files '{"filePath": "file1.py", "fileMode": "EXECUTABLE", "fileContent": "bucket_name = sys.argv[1] region = sys.argv[2]"}'
'{"filePath": "file2.txt", "fileMode": "NORMAL", "fileContent": "//Adding a comment to explain the variable changes in file1.py"}' '{"filePath": "images/image1.png",
"fileMode": "NORMAL", "sourceFile": {"filePath": "pictures/picture.png", "isMove": true}}' --delete-files filePath="ExampleSolution.py"
```

`ファイルの名前を picture.png から image1.png に変更し、それを pictures という名前のディレクトリから images という名前のディレクトリに移動`
↓
`'{"filePath": "images/image1.png","fileMode": "NORMAL", "sourceFile": {"filePath": "pictures/picture.png", "isMove": true}}'`

ん？`sourceFile`にリポジトリ内のファイルパスを指定している？

### awscliで動作確認

```
$ tree <リポジトリ名>/
<リポジトリ名>/
├── fuga
│   └── piyo.txt
└── hoge.txt

1 directory, 2 files
$ cat <リポジトリ名>/hoge.txt
hoge
```

ファイルの名前を hoge.txt から　hogera.txt に変更し、それを fuga という名前のディレクトリに移動させる。

```
$ aws codecommit create-commit \
> --repository-name <リポジトリ名> \
> --branch-name <ブランチ名> \
> --parent-commit-id <親コミットID> \
> --put-files '[{"filePath": "fuga/hogera.txt", "sourceFile": {"filePath": "hoge.txt", "isMove": true}}]'
{
    "commitId": "bf2084b422d8c120bdb0771ce1808d269dd0ee31",
    "treeId": "8176bcf69b9423bb79a29044a984fc0452cf6a0b",
    "filesAdded": [
        {
            "absolutePath": "fuga/hogera.txt",
            "blobId": "2262de0c121f22df8e78f5a37d6e114fd322c0b0",
            "fileMode": "NORMAL"
        }
    ],
    "filesUpdated": [],
    "filesDeleted": [
        {
            "absolutePath": "hoge.txt",
            "blobId": "2262de0c121f22df8e78f5a37d6e114fd322c0b0",
            "fileMode": "NORMAL"
        }
    ]
}
```

ローカルブランチをリモートブランチと同期してから、

```
$ git pull
```

ファイルを確認すると、

```
$ tree <リポジトリ名>/
<リポジトリ名>/
└── fuga
    ├── hogera.txt
    └── piyo.txt

1 directory, 2 files
$ cat <リポジトリ名>/fuga/hogera.txt 
hoge
```

hoge.txt が　hogera.txt にリネームされ、かつ、 fuga ディレクトリに移動されていることが確認できた。

**つまり、`putFiles`->`sourceFile`->`filePath`には、ローカルのファイルではなく、リポジトリ内のファイルを指定する仕様である。**

だから、エラーメッセージに表示されたソースファイルのパス（`tmp/piyo01.json`）には、スラッシュ(/)がなくなっていた。
※リポジトリ内のルートディレクトリからの相対パスになっている。

## 回避方法

ローカルのファイルをCodeCommitリポジトリに追加(更新)する方法として、`sourceFile`ではなく、`fileContent`でコンテンツの内容を直接指定する方法に変えれば、コミットできた。

```
    for (root, folders, files) in os.walk('/tmp/'):
        for file in files:
            file_path = os.path.join(root, file)
            with open(file_path, mode='r+b') as file_obj:
                file_content = file_obj.read()
            putfile_entry = {'filePath': str(file_path).replace('/tmp/', 'piyo/'),
                            'fileContent': file_content}
            putfile_list.append(putfile_entry)
    
    return cc.create_commit(repositoryName='<リポジトリ名>', branchName='<ブランチ名>', parentCommitId=commit_id, putFiles=putfile_list)
```

ただし、上記コードは /tmp ディレクトリ配下のすべてのファイルを更新するため、すべてのファイルで何らか変更されている必要がある。

変更されていないファイルがあった場合、以下エラーとなる。

```
[ERROR] SamePathRequestException: An error occurred (SamePathRequestException) when calling the CreateCommit operation: Cannot put the file at the specified path because there are one or more conflicting file change requests for that file. Either resolve the conflicts locally, or put the files you want to add in different paths, and then try again.. Path: piyo/piyo01.json
Traceback (most recent call last):
  File "/var/task/lambda_function.py", line 103, in lambda_handler
    return cc.create_commit(repositoryName=repo_name, branchName=branch_name, parentCommitId=commit_id, putFiles=putfile_list)
  File "/var/runtime/botocore/client.py", line 357, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/var/runtime/botocore/client.py", line 676, in _make_api_call
    raise error_class(parsed_response, operation_name)END RequestId: b16063f3-e356-4117-95fb-a087ce0c8401
```