mirror of
https://github.com/danog/gift.git
synced 2024-11-30 04:19:37 +01:00
03a024e7a3
Fix error when getting the current branch but it is checked out to a commit
664 lines
21 KiB
CoffeeScript
664 lines
21 KiB
CoffeeScript
should = require 'should'
|
|
sinon = require 'sinon'
|
|
|
|
fs = require 'fs-extra'
|
|
fixtures = require './fixtures'
|
|
git = require '../src'
|
|
Actor = require '../src/actor'
|
|
Commit = require '../src/commit'
|
|
Tree = require '../src/tree'
|
|
Diff = require '../src/diff'
|
|
Tag = require '../src/tag'
|
|
Status = require '../src/status'
|
|
|
|
{Ref, Head} = require '../src/ref'
|
|
{exec} = require 'child_process'
|
|
|
|
describe "Repo", ->
|
|
|
|
describe "#add", ->
|
|
repo = null
|
|
git_dir = __dirname + "/fixtures/junk_add"
|
|
status = null
|
|
file = null
|
|
|
|
# given a fresh new repo
|
|
before (done) ->
|
|
fs.remove git_dir, (err) ->
|
|
return done err if err
|
|
fs.mkdir git_dir, '0755', (err) ->
|
|
return done err if err
|
|
git.init git_dir, (err) ->
|
|
return done err if err
|
|
repo = git git_dir
|
|
done()
|
|
|
|
after (done) ->
|
|
fs.remove git_dir, done
|
|
|
|
describe "with only a file", ->
|
|
file = 'foo.txt'
|
|
# given a new file
|
|
before (done) ->
|
|
fs.writeFile "#{git_dir}/#{file}", "cheese", (err) ->
|
|
return done err if err?
|
|
repo.add "#{git_dir}/#{file}", (err) ->
|
|
return done err if err?
|
|
repo.status (err, _status) ->
|
|
status = _status
|
|
done err
|
|
|
|
it "was added", ->
|
|
status.files.should.have.a.property file
|
|
status.files[file].staged.should.be.true
|
|
status.files[file].tracked.should.be.true
|
|
status.files[file].type.should.eql 'A'
|
|
|
|
describe "with no file and all option", ->
|
|
file = 'bar.txt'
|
|
# given a new file
|
|
before (done) ->
|
|
fs.writeFile "#{git_dir}/#{file}", "cheese", (err) ->
|
|
return done err if err?
|
|
repo.add [], A:true, (err) ->
|
|
return done err if err?
|
|
repo.status (err, _status) ->
|
|
status = _status
|
|
done err
|
|
|
|
it "was added", ->
|
|
status.files.should.have.a.property file
|
|
status.files[file].staged.should.be.true
|
|
status.files[file].tracked.should.be.true
|
|
status.files[file].type.should.eql 'A'
|
|
|
|
describe "#sync", ->
|
|
describe "when passed curried arguments", ->
|
|
repo = fixtures.branched
|
|
remote = branch = ""
|
|
|
|
before ->
|
|
sinon.stub repo, "git", (command, opts, args, callback) ->
|
|
if command is "pull"
|
|
remote = args[0]
|
|
branch = args[1]
|
|
callback? null
|
|
sinon.stub repo, "status", (callback) ->
|
|
callback? null, clean: no
|
|
|
|
after ->
|
|
repo.git.restore()
|
|
repo.status.restore()
|
|
|
|
it "passes through the correct parameters when nothing is omitted", (done) ->
|
|
repo.sync "github", "my-branch", ->
|
|
remote.should.eql "github"
|
|
branch.should.eql "my-branch"
|
|
done()
|
|
|
|
it "passes through the correct parameters when remote_name is omitted", (done) ->
|
|
repo.sync "my-branch", ->
|
|
remote.should.eql "origin"
|
|
branch.should.eql "my-branch"
|
|
done()
|
|
|
|
it "passes through the correct parameters when remote_name and branch are omitted", (done) ->
|
|
repo.sync ->
|
|
remote.should.eql "origin"
|
|
branch.should.eql "master"
|
|
done()
|
|
|
|
|
|
describe "#identify", ->
|
|
describe "when asked to set the identity's name and email", ->
|
|
repo = fixtures.branched
|
|
id = '' + new Date().getTime()
|
|
name = "name-#{id}"
|
|
email = "#{id}@domain"
|
|
ident = null
|
|
|
|
before (done) ->
|
|
actor = new Actor(name, email)
|
|
repo.identify actor, (err) ->
|
|
done err if err
|
|
repo.identity (err, _actor) ->
|
|
ident = _actor
|
|
done err
|
|
|
|
after (done) ->
|
|
exec "git checkout -- #{ repo.path }", done
|
|
|
|
it "has correctly set them", ->
|
|
ident.name.should.eql name
|
|
ident.email.should.eql email
|
|
|
|
describe "#commits", ->
|
|
|
|
describe "with a single commit", ->
|
|
repo = null
|
|
commit = null
|
|
git_dir = __dirname + "/fixtures/junk_commit"
|
|
|
|
# given a fresh new repo
|
|
before (done) ->
|
|
fs.remove git_dir, (err) ->
|
|
return done err if err?
|
|
fs.mkdir git_dir, '0755', (err) ->
|
|
return done err if err?
|
|
git.init git_dir, (err) ->
|
|
return done err if err?
|
|
repo = git(git_dir)
|
|
fs.writeFileSync "#{git_dir}/foo.txt", "cheese"
|
|
repo.identify new Actor('root', 'root@domain.net'), (err) ->
|
|
return done err if err?
|
|
repo.add "#{git_dir}/foo.txt", (err) ->
|
|
return done err if err?
|
|
repo.commit 'message with spaces',
|
|
author: 'Someone <someone@somewhere.com>'
|
|
, (err) ->
|
|
return done err if err?
|
|
repo.commits (err, _commits) ->
|
|
commit = _commits[0]
|
|
done err
|
|
|
|
after (done) ->
|
|
fs.remove git_dir, done
|
|
|
|
it "has right message", (done) ->
|
|
commit.message.should.eql 'message with spaces'
|
|
commit.author.name.should.eql 'Someone'
|
|
commit.author.email.should.eql 'someone@somewhere.com'
|
|
done()
|
|
|
|
it "has a tree", (done) ->
|
|
commit.tree().should.be.an.instanceof Tree
|
|
commit.tree().contents (err, child) ->
|
|
return done err if err
|
|
child.length.should.eql 1
|
|
child[0].name.should.eql 'foo.txt'
|
|
done()
|
|
describe "with only a callback", ->
|
|
repo = fixtures.branched
|
|
commits = null
|
|
before (done) ->
|
|
repo.commits (err, _commits) ->
|
|
commits = _commits
|
|
done err
|
|
|
|
it "passes an Array", ->
|
|
commits.should.be.an.instanceof Array
|
|
|
|
it "is a list of commits", ->
|
|
commits[0].id.should.eql "913318e66e9beed3e89e9c402c1d6585ef3f7e6f"
|
|
commits[0].repo.should.eql repo
|
|
commits[0].author.name.should.eql "sentientwaffle"
|
|
commits[0].committer.name.should.eql "sentientwaffle"
|
|
commits[0].authored_date.should.be.an.instanceof Date
|
|
commits[0].committed_date.should.be.an.instanceof Date
|
|
commits[0].parents().should.be.an.instanceof Array
|
|
commits[0].message.should.eql "add a sub dir"
|
|
|
|
describe "specify a branch", ->
|
|
repo = fixtures.branched
|
|
commits = null
|
|
before (done) ->
|
|
repo.commits "something", (err, _commits) ->
|
|
commits = _commits
|
|
done err
|
|
|
|
# The first commit ...
|
|
it "is the latest commit", ->
|
|
commits[0].message.should.eql "2"
|
|
|
|
it "has a parent commit", ->
|
|
commits[0].parents().should.have.lengthOf 1
|
|
commits[0].parents()[0].id.should.eql commits[1].id
|
|
|
|
describe "specify a tag", ->
|
|
repo = fixtures.tagged
|
|
commits = null
|
|
before (done) ->
|
|
repo.commits "tag-1", (err, _commits) ->
|
|
commits = _commits
|
|
done err
|
|
|
|
it "is the latest commit on the tag", ->
|
|
commits[0].message.should.containEql "commit 5"
|
|
|
|
describe "limit the number of commits", ->
|
|
repo = fixtures.tagged
|
|
commits = null
|
|
before (done) ->
|
|
repo.commits "master", 2, (err, _commits) ->
|
|
commits = _commits
|
|
done err
|
|
|
|
it "returns 2 commits", ->
|
|
commits.should.have.lengthOf 2
|
|
|
|
describe "skip commits", ->
|
|
repo = fixtures.tagged
|
|
commits = null
|
|
before (done) ->
|
|
repo.commits "master", 1, 2, (err, _commits) ->
|
|
commits = _commits
|
|
done err
|
|
|
|
it "returns 2 commits", ->
|
|
commits[0].message.should.containEql "commit 4"
|
|
|
|
describe "with or without gpg signature", ->
|
|
repo = fixtures.gpgsigned
|
|
commits = null
|
|
before (done) ->
|
|
repo.commits "master", (err, _commits) ->
|
|
commits = _commits
|
|
done err
|
|
|
|
it "has no gpgsig", ->
|
|
commits[0].gpgsig.should.not.be.ok
|
|
|
|
it "has gpgsig", ->
|
|
commits[1].gpgsig.should.be.ok
|
|
|
|
it "contains the correct signature", ->
|
|
commits[1].gpgsig.should.equal """
|
|
-----BEGIN#{" "}PGP#{" "}SIGNATURE-----
|
|
#{" "}Version:#{" "}GnuPG#{" "}v2.0.22#{" "}(GNU/Linux)
|
|
#{" "}
|
|
#{" "}iQEcBAABAgAGBQJTQw8qAAoJEL0/h9tqDFPiP3UH/RwxUS90+6DEkThcKMmV9H4K
|
|
#{" "}dr+D0H0z2ViMq3AHSmCydv5dWr3bupl2XyaLWWuRCxAJ78xuf98qVRIBfT/FKGeP
|
|
#{" "}fz+GtXkv3naCD12Ay6YiwfxSQhxFiJtRwP5rla2i7hlV3BLFPYCWTtL8OLF4CoRm
|
|
#{" "}7aF5EuDr1x7emEDyu1rf5E59ttSIySuIw0J1mTjrPCkC6lsowzTJS/vaCxZ3e7fN
|
|
#{" "}iZE6VEWWY/iOxd8foJH/VZ3cfNKjfi8+Fh8t7o9ztjYTQAOZUJTn2CHB7Wkyr0Ar
|
|
#{" "}HNM3v26gPFpb7UkHw0Cq2HWNV/Z7cbQc/BQ4HmrmuBPB6SWNOaBN751BbQKnPcA=
|
|
#{" "}=IusH
|
|
#{" "}-----END#{" "}PGP#{" "}SIGNATURE-----"""
|
|
|
|
describe "#tree", ->
|
|
repo = fixtures.branched
|
|
describe "master", ->
|
|
it "is a Tree", ->
|
|
repo.tree().should.be.an.instanceof Tree
|
|
|
|
it "checks out branch:master", (done) ->
|
|
repo.tree().blobs (err, blobs) ->
|
|
blobs[0].data (err, data) ->
|
|
data.should.containEql "Bla"
|
|
data.should.not.containEql "Bla2"
|
|
done err
|
|
|
|
describe "specific branch", ->
|
|
it "is a Tree", ->
|
|
repo.tree("something").should.be.an.instanceof Tree
|
|
|
|
it "checks out branch:something", (done) ->
|
|
repo.tree("something").blobs (err, blobs) ->
|
|
blobs[0].data (err, data) ->
|
|
data.should.containEql "Bla2"
|
|
done err
|
|
|
|
|
|
describe "#diff", ->
|
|
repo = fixtures.branched
|
|
describe "between 2 branches", ->
|
|
diffs = null
|
|
before (done) ->
|
|
repo.diff "something", "master", (err, _diffs) ->
|
|
diffs = _diffs
|
|
done err
|
|
|
|
it "is passes an Array of Diffs", ->
|
|
diffs.should.be.an.instanceof Array
|
|
diffs[0].should.be.an.instanceof Diff
|
|
|
|
# The first diff...
|
|
it "modifies the README.md file", ->
|
|
diffs[0].a_path.should.eql "README.md"
|
|
diffs[0].b_path.should.eql "README.md"
|
|
|
|
# The second diff...
|
|
it "creates some/hi.txt", ->
|
|
diffs[1].new_file.should.be.true
|
|
diffs[1].b_path.should.eql "some/hi.txt"
|
|
|
|
|
|
describe "#remotes", ->
|
|
describe "in a repository with remotes", ->
|
|
repo = fixtures.remotes
|
|
remotes = null
|
|
before (done) ->
|
|
repo.remotes (err, _remotes) ->
|
|
remotes = _remotes
|
|
done err
|
|
|
|
it "is an Array of Refs", ->
|
|
remotes.should.be.an.instanceof Array
|
|
remotes[0].should.be.an.instanceof Ref
|
|
|
|
it "contains the correct Refs", ->
|
|
remotes[0].commit.id.should.eql "bdd3996d38d885e18e5c5960df1c2c06e34d673f"
|
|
remotes[0].name.should.eql "origin/HEAD"
|
|
remotes[1].commit.id.should.eql "bdd3996d38d885e18e5c5960df1c2c06e34d673f"
|
|
remotes[1].name.should.eql "origin/master"
|
|
|
|
describe "when there are no remotes", ->
|
|
repo = fixtures.branched
|
|
it "is an empty Array", ->
|
|
repo.remotes (err, remotes) ->
|
|
remotes.should.eql []
|
|
|
|
|
|
describe "#remote_list", ->
|
|
describe "in a repository with remotes", ->
|
|
repo = fixtures.remotes
|
|
remotes = null
|
|
before (done) ->
|
|
repo.remote_list (err, _remotes) ->
|
|
remotes = _remotes
|
|
done err
|
|
|
|
it "is a list of remotes", ->
|
|
remotes.should.have.lengthOf 1
|
|
remotes[0].should.eql "origin"
|
|
|
|
describe "when there are no remotes", ->
|
|
repo = fixtures.branched
|
|
it "is an empty Array", ->
|
|
repo.remote_list (err, remotes) ->
|
|
remotes.should.eql []
|
|
|
|
|
|
describe "#tags", ->
|
|
describe "a repo with tags", ->
|
|
repo = fixtures.tagged
|
|
tags = null
|
|
before (done) ->
|
|
repo.tags (err, _tags) ->
|
|
tags = _tags
|
|
done err
|
|
|
|
it "is an Array of Tags", ->
|
|
tags.should.be.an.instanceof Array
|
|
tags[0].should.be.an.instanceof Tag
|
|
|
|
it "is the correct tag", ->
|
|
tags[0].name.should.eql "tag-1"
|
|
|
|
describe "a repo without tags", ->
|
|
repo = fixtures.branched
|
|
it "is an empty array", (done) ->
|
|
repo.tags (err, tags) ->
|
|
tags.should.eql []
|
|
done err
|
|
|
|
describe "#create_tag", ->
|
|
repo = null
|
|
git_dir = __dirname + "/fixtures/junk_create_tag"
|
|
|
|
before (done) ->
|
|
fs.remove git_dir, (err) ->
|
|
return done err if err
|
|
fs.mkdir git_dir, 0o755, (err) ->
|
|
return done err if err
|
|
git.init git_dir, (err) ->
|
|
return done err if err
|
|
repo = git(git_dir)
|
|
repo.identify new Actor('name', 'em@il'), ->
|
|
fs.writeFileSync "#{git_dir}/foo.txt", "cheese"
|
|
repo.add "#{git_dir}/foo.txt", (err) ->
|
|
return done err if err
|
|
repo.commit "initial commit", {all: true}, done
|
|
|
|
after (done) ->
|
|
fs.remove git_dir, done
|
|
|
|
it "creates a tag", (done) ->
|
|
repo.create_tag "foo", done
|
|
|
|
describe "#delete_tag", ->
|
|
describe "deleting a tag that does not exist", ->
|
|
repo = fixtures.branched
|
|
it "passes an error", (done) ->
|
|
repo.delete_tag "nonexistant-tag", (err) ->
|
|
should.exist err
|
|
done()
|
|
|
|
|
|
describe "#branches", ->
|
|
repo = fixtures.branched
|
|
branches = null
|
|
before (done) ->
|
|
repo.branches (err, _branches) ->
|
|
branches = _branches
|
|
done err
|
|
|
|
it "is an Array of Heads", ->
|
|
branches.should.be.an.instanceof Array
|
|
branches[0].should.be.an.instanceof Head
|
|
|
|
it "has the correct branches", ->
|
|
branches[0].name.should.eql "master"
|
|
branches[1].name.should.eql "something"
|
|
|
|
|
|
describe "#branch", ->
|
|
describe "when a branch name is given", ->
|
|
repo = fixtures.branched
|
|
branch = null
|
|
before (done) ->
|
|
repo.branch "something", (err, b) ->
|
|
branch = b
|
|
done err
|
|
|
|
it "is a Head", ->
|
|
branch.should.be.an.instanceof Head
|
|
|
|
it "has the correct name", ->
|
|
branch.name.should.eql "something"
|
|
|
|
describe "when no branch name is given", ->
|
|
repo = fixtures.branched
|
|
branch = null
|
|
before (done) ->
|
|
repo.branch (err, b) ->
|
|
branch = b
|
|
done err
|
|
|
|
it "has the correct name", ->
|
|
branch.name.should.eql "master"
|
|
|
|
describe "an invalid branch", ->
|
|
repo = fixtures.branched
|
|
it "passes an error", (done) ->
|
|
repo.branch "nonexistant-branch", (err, b) ->
|
|
should.exist err
|
|
should.not.exist b
|
|
done()
|
|
|
|
|
|
describe "current branch is (no branch)", ->
|
|
repo = fixtures.noBranch
|
|
it "passes an error", (done) ->
|
|
repo.branch (err, b) ->
|
|
should.exist err
|
|
should.not.exist b
|
|
done()
|
|
|
|
|
|
describe "#delete_branch", ->
|
|
describe "a branch that does not exist", ->
|
|
repo = fixtures.branched
|
|
it "passes an error", (done) ->
|
|
repo.delete_branch "nonexistant-branch", (err) ->
|
|
should.exist err
|
|
done()
|
|
|
|
describe "#reset", ->
|
|
repo = null
|
|
git_dir = __dirname + "/fixtures/junk_reset"
|
|
status = null
|
|
file = "bla.txt"
|
|
|
|
# given a fresh new repo
|
|
beforeEach (done) ->
|
|
status = null
|
|
fs.remove git_dir, (err) ->
|
|
return done err if err
|
|
fs.copy "#{__dirname}/fixtures/reset", "#{git_dir}", (err) ->
|
|
return done err if err
|
|
fs.rename "#{git_dir}/git.git", "#{git_dir}/.git", (err) ->
|
|
return done err if err
|
|
git.init git_dir, (err) ->
|
|
repo = git git_dir
|
|
fs.writeFile "#{git_dir}/#{file}", "hello", (err) ->
|
|
return done err if err?
|
|
repo.add "#{git_dir}/#{file}", (err) ->
|
|
done err
|
|
|
|
after (done) ->
|
|
fs.remove git_dir, (err) ->
|
|
done err
|
|
|
|
describe "reset without specific treeish (defaults to HEAD)", ->
|
|
describe "reset (--mixed)", ->
|
|
beforeEach (done) ->
|
|
repo.reset ->
|
|
repo.status (err, _status) ->
|
|
status = _status
|
|
done err
|
|
|
|
it "removes the file from index, leaves it in working tree", ->
|
|
status.files.should.have.a.property file
|
|
status.files[file].staged.should.be.false
|
|
status.files[file].tracked.should.be.false
|
|
status.files[file].should.not.have.a.property 'type'
|
|
|
|
describe "reset --soft", ->
|
|
beforeEach (done) ->
|
|
repo.reset {soft: true}, ->
|
|
repo.status (err, _status) ->
|
|
status = _status
|
|
done err
|
|
|
|
it "leaves the added file in the index", ->
|
|
status.files.should.have.a.property file
|
|
status.files[file].staged.should.be.true
|
|
status.files[file].tracked.should.be.true
|
|
status.files[file].type.should.eql 'A'
|
|
|
|
describe "reset --hard", ->
|
|
beforeEach (done) ->
|
|
repo.reset {hard: true}, ->
|
|
repo.status (err, _status) ->
|
|
status = _status
|
|
done err
|
|
|
|
it "removes the file from index and working tree", ->
|
|
status.files.should.not.have.a.property file
|
|
|
|
describe "reset to specific treeish", ->
|
|
describe "reset (--mixed) HEAD~1", ->
|
|
beforeEach (done) ->
|
|
repo.reset 'HEAD~1', ->
|
|
repo.status (err, _status) ->
|
|
status = _status
|
|
done err
|
|
|
|
it "resets to HEAD~1, changes stay in the working tree", ->
|
|
status.files.should.have.a.property file
|
|
status.files[file].staged.should.be.false
|
|
status.files[file].tracked.should.be.false
|
|
status.files[file].should.not.have.a.property 'type'
|
|
|
|
status.files.should.have.a.property 'rawr.txt'
|
|
status.files['rawr.txt'].staged.should.be.false
|
|
status.files['rawr.txt'].tracked.should.be.false
|
|
status.files['rawr.txt'].should.not.have.a.property 'type'
|
|
|
|
describe "reset --soft HEAD~1", ->
|
|
beforeEach (done) ->
|
|
repo.reset 'HEAD~1', {soft: true}, ->
|
|
repo.status (err, _status) ->
|
|
status = _status
|
|
done err
|
|
|
|
it "resets to HEAD~1, changes stay in the index and working tree", ->
|
|
status.files.should.have.a.property file
|
|
status.files[file].staged.should.be.true
|
|
status.files[file].tracked.should.be.true
|
|
status.files[file].type.should.eql 'A'
|
|
|
|
status.files.should.have.a.property 'rawr.txt'
|
|
status.files['rawr.txt'].staged.should.be.true
|
|
status.files['rawr.txt'].tracked.should.be.true
|
|
status.files['rawr.txt'].type.should.eql 'AM'
|
|
|
|
describe "reset --hard HEAD~1", ->
|
|
beforeEach (done) ->
|
|
repo.reset 'HEAD~1', {hard: true}, ->
|
|
repo.status (err, _status) ->
|
|
status = _status
|
|
done err
|
|
|
|
it "resets to HEAD~1, all changes get discarded completely", ->
|
|
status.files.should.not.have.a.property file
|
|
status.files.should.not.have.a.property 'rawr.txt'
|
|
|
|
describe "#checkoutFile", ->
|
|
repo = null
|
|
git_dir = __dirname + "/fixtures/junk_checkoutFile"
|
|
status = null
|
|
file = "bla.txt"
|
|
|
|
# given a fresh new repo
|
|
beforeEach (done) ->
|
|
status = null
|
|
fs.remove git_dir, (err) ->
|
|
return done err if err
|
|
fs.copy "#{__dirname}/fixtures/reset", "#{git_dir}", (err) ->
|
|
return done err if err
|
|
fs.rename "#{git_dir}/git.git", "#{git_dir}/.git", (err) ->
|
|
git.init git_dir, (err) ->
|
|
return done err if err
|
|
repo = git git_dir
|
|
fs.writeFile "#{git_dir}/#{file}", "hello", (err) ->
|
|
return done err if err?
|
|
repo.add "#{git_dir}/#{file}", (err) ->
|
|
done err
|
|
|
|
after (done) ->
|
|
fs.remove git_dir, (err) ->
|
|
done err
|
|
|
|
describe "passing no explicit files", ->
|
|
beforeEach (done) ->
|
|
repo.checkoutFile ->
|
|
repo.status (err, _status) ->
|
|
status = _status
|
|
done err
|
|
|
|
it "discards changes in the working tree for all files", ->
|
|
status.files.should.have.a.property file
|
|
status.files[file].staged.should.be.true
|
|
status.files[file].tracked.should.be.true
|
|
status.files[file].type.should.eql 'A'
|
|
|
|
status.files.should.have.a.property 'rawr.txt'
|
|
status.files['rawr.txt'].staged.should.be.true
|
|
status.files['rawr.txt'].tracked.should.be.true
|
|
status.files['rawr.txt'].type.should.eql 'M'
|
|
|
|
describe "passing an explicit file", ->
|
|
beforeEach (done) ->
|
|
repo.checkoutFile 'rawr.txt', ->
|
|
repo.status (err, _status) ->
|
|
status = _status
|
|
done err
|
|
|
|
it "discard changes to the specified file", ->
|
|
status.files.should.have.a.property 'rawr.txt'
|
|
status.files['rawr.txt'].staged.should.be.true
|
|
status.files['rawr.txt'].tracked.should.be.true
|
|
status.files['rawr.txt'].type.should.eql 'M'
|