<commit_before>const Eris = require('eris');
const moment = require('moment');
const transliterate = require('transliteration');

const config = require('./config');
const bot = require('./bot');
const Queue = require('./queue');
const utils = require('./utils');
const threadUtils = require('./threadUtils');
const blocked = require('./data/blocked');
const threads = require('./data/threads');

const snippets = require('./plugins/snippets');
const webserver = require('./plugins/webserver');
const greeting = require('./plugins/greeting');
const attachments = require("./data/attachments");
const {ACCIDENTAL_THREAD_MESSAGES} = require('./data/constants');

const messageQueue = new Queue();

const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args);

// Once the bot has connected, set the status/"playing" message
bot.on('ready', () => {
  bot.editStatus(null, {name: config.status});
});

/**
 * When a moderator posts in a modmail thread...
 * 1) If alwaysReply is enabled, reply to the user
 * 2) If alwaysReply is disabled, save that message as a chat message in the thread
 */
bot.on('messageCreate', async msg => {
  if (! utils.messageIsOnInboxServer(msg)) return;
  if (! utils.isStaff(msg.member)) return;
  if (msg.author.bot) return;
  if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) return;

  const thread = await threads.findByChannelId(msg.channel.id);
  if (! thread) return;

  if (config.alwaysReply) {
    // AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies
    if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
    await thread.replyToUser(msg.member, msg.content.trim(), msg.attachments, config.alwaysReplyAnon || false);
    msg.delete();
  } else {
    // Otherwise just save the messages as "chat" in the logs
    thread.saveChatMessage(msg);
  }
});

/**
 * When we get a private message...
 * 1) Find the open modmail thread for this user, or create a new one
 * 2) Post the message as a user reply in the thread
 */
bot.on('messageCreate', async msg => {
  if (! (msg.channel instanceof Eris.PrivateChannel)) return;
  if (msg.author.bot) return;
  if (msg.type !== 0) return; // Ignore pins etc.

  if (await blocked.isBlocked(msg.author.id)) return;

  // Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
  messageQueue.add(async () => {
    let thread = await threads.findOpenThreadByUserId(msg.author.id);

    // New thread
    if (! thread) {
      // Ignore messages that shouldn't usually open new threads, such as "ok", "thanks", etc.
      if (config.ignoreAccidentalThreads && msg.content && ACCIDENTAL_THREAD_MESSAGES.includes(msg.content.trim())) return;

      thread = await threads.createNewThreadForUser(msg.author);
    }

    await thread.receiveUserReply(msg);
  });
});

/**
 * When a message is edited...
 * 1) If that message was in DMs, and we have a thread open with that user, post the edit as a system message in the thread
 * 2) If that message was moderator chatter in the thread, update the corresponding chat message in the DB
 */
bot.on('messageUpdate', async (msg, oldMessage) => {
  if (! msg || ! msg.author) return;
  if (msg.author.bot) return;
  if (await blocked.isBlocked(msg.author.id)) return;

  // Old message content doesn't persist between bot restarts
  const oldContent = oldMessage && oldMessage.content || '*Unavailable due to bot restart*';
  const newContent = msg.content;

  // Ignore bogus edit events with no changes
  if (newContent.trim() === oldContent.trim()) return;

  // 1) Edit in DMs
  if (msg.channel instanceof Eris.PrivateChannel) {
    const thread = await threads.findOpenThreadByUserId(msg.author.id);
    const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`);

    thread.postSystemMessage(editMessage);
  }

  // 2) Edit in the thread
  else if (utils.messageIsOnInboxServer(msg) && utils.isStaff(msg.member)) {
    const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
    if (! thread) return;

    thread.updateChatMessage(msg);
  }
});

/**
 * When a staff message is deleted in a modmail thread, delete it from the database as well
 */
bot.on('messageDelete', async msg => {
  if (! msg.author) return;
  if (msg.author.bot) return;
  if (! utils.messageIsOnInboxServer(msg)) return;
  if (! utils.isStaff(msg.member)) return;

  const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
  if (! thread) return;

  thread.deleteChatMessage(msg.id);
});

/**
 * When the bot is mentioned on the main server, ping staff in the log channel about it
 */
bot.on('messageCreate', async msg => {
  if (! utils.messageIsOnMainServer(msg)) return;
  if (! msg.mentions.some(user => user.id === bot.user.id)) return;

  // If the person who mentioned the modmail bot is also on the modmail server, ignore them
  if (utils.getInboxGuild().members.get(msg.author.id)) return;

  // If the person who mentioned the bot is blocked, ignore them
  if (await blocked.isBlocked(msg.author.id)) return;

  bot.createMessage(utils.getLogChannel(bot).id, {
    content: `@here Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}**: "${msg.cleanContent}"`,
    disableEveryone: false,
  });
});

// Mods can reply to modmail threads using !r or !reply
// These messages get relayed back to the DM thread between the bot and the user
addInboxServerCommand('reply', async (msg, args, thread) => {
  if (! thread) return;

  const text = args.join(' ').trim();
  if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
  await thread.replyToUser(msg.member, text, msg.attachments, false);
  msg.delete();
});

bot.registerCommandAlias('r', 'reply');

// Anonymous replies only show the role, not the username
addInboxServerCommand('anonreply', async (msg, args, thread) => {
  if (! thread) return;

  const text = args.join(' ').trim();
  if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
  await thread.replyToUser(msg.member, text, msg.attachments, true);
  msg.delete();
});

bot.registerCommandAlias('ar', 'anonreply');

// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel.
addInboxServerCommand('close', async (msg, args, thread) => {
  if (! thread) return;
  await thread.close();

  const logUrl = await thread.getLogUrl();
  utils.postLog(utils.trimAll(`
    Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${msg.author.username}
    Logs: ${logUrl}
  `));
});

addInboxServerCommand('block', (msg, args, thread) => {
  async function block(userId) {
    await blocked.block(userId);
    msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`);
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    block(userId);
  } else if (thread) {
    // Calling !block without args in a modmail thread blocks the user of that thread
    block(thread.user_id);
  }
});

addInboxServerCommand('unblock', (msg, args, thread) => {
  async function unblock(userId) {
    await blocked.unblock(userId);
    msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`);
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    unblock(userId);
  } else if (thread) {
    // Calling !unblock without args in a modmail thread unblocks the user of that thread
    unblock(thread.user_id);
  }
});

addInboxServerCommand('logs', (msg, args, thread) => {
  async function getLogs(userId) {
    const userThreads = await threads.getClosedThreadsByUserId(userId);

    // Descending by date
    userThreads.sort((a, b) => {
      if (a.created_at > b.created_at) return -1;
      if (a.created_at < b.created_at) return 1;
      return 0;
    });

    const threadLines = await Promise.all(userThreads.map(async thread => {
      const logUrl = await thread.getLogUrl();
      const formattedDate = moment.utc(thread.created_at).format('MMM Do [at] HH:mm [UTC]');
      return `\`${formattedDate}\`: <${logUrl}>`;
    }));

    const message = `**Log files for <@${userId}>:**\n${threadLines.join('\n')}`;

    // Send the list of logs in chunks of 15 lines per message
    const lines = message.split('\n');
    const chunks = utils.chunk(lines, 15);

    let root = Promise.resolve();
    chunks.forEach(lines => {
      root = root.then(() => msg.channel.createMessage(lines.join('\n')));
    });
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    getLogs(userId);
  } else if (thread) {
    // Calling !logs without args in a modmail thread returns the logs of the user of that thread
    getLogs(thread.user_id);
  }
});

addInboxServerCommand('move', async (msg, args, thread) => {
  if (! config.allowMove) return;

  if (! thread) return;

  const searchStr = args[0];
  if (! searchStr || searchStr.trim() === '') return;

  const normalizedSearchStr = transliterate.slugify(searchStr);

  const categories = msg.channel.guild.channels.filter(c => {
    // Filter to categories that are not the thread's current parent category
    return (c instanceof Eris.CategoryChannel) && (c.id !== msg.channel.parentID);
  });

  if (categories.length === 0) return;

  // See if any category name contains a part of the search string
  const containsRankings = categories.map(cat => {
    const normalizedCatName = transliterate.slugify(cat.name);

    let i;
    for (i = 1; i < normalizedSearchStr.length; i++) {
      if (! normalizedCatName.includes(normalizedSearchStr.slice(0, i))) {
        i--;
        break;
      }
    }

    return [cat, i];
  });

  // Sort by best match
  containsRankings.sort((a, b) => {
    return a[1] > b[1] ? -1 : 1;
  });

  if (containsRankings[0][1] === 0) {
    thread.postNonLogMessage('No matching category');
    return;
  }

  const targetCategory = containsRankings[0][0];

  await bot.editChannel(thread.channel_id, {
    parentID: targetCategory.id
  });

  thread.postSystemMessage(`Thread moved to ${targetCategory.name.toUpperCase()}`);
});

addInboxServerCommand('loglink', async (msg, args, thread) => {
  if (! thread) return;
  const logUrl = await thread.getLogUrl();
  thread.postNonLogMessage(`Log URL: ${logUrl}`);
});

module.exports = {
  async start() {
    // Load plugins
    console.log('Loading plugins...');
    await snippets(bot);
    await greeting(bot);
    await webserver(bot);

    console.log('Connecting to Discord...');
    await bot.connect();

    console.log('Done! Now listening to DMs.');
  }
};
<commit_msg>Fix accidental message checking not being case-insensitive<commit_after>const Eris = require('eris');
const moment = require('moment');
const transliterate = require('transliteration');

const config = require('./config');
const bot = require('./bot');
const Queue = require('./queue');
const utils = require('./utils');
const threadUtils = require('./threadUtils');
const blocked = require('./data/blocked');
const threads = require('./data/threads');

const snippets = require('./plugins/snippets');
const webserver = require('./plugins/webserver');
const greeting = require('./plugins/greeting');
const attachments = require("./data/attachments");
const {ACCIDENTAL_THREAD_MESSAGES} = require('./data/constants');

const messageQueue = new Queue();

const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args);

// Once the bot has connected, set the status/"playing" message
bot.on('ready', () => {
  bot.editStatus(null, {name: config.status});
});

/**
 * When a moderator posts in a modmail thread...
 * 1) If alwaysReply is enabled, reply to the user
 * 2) If alwaysReply is disabled, save that message as a chat message in the thread
 */
bot.on('messageCreate', async msg => {
  if (! utils.messageIsOnInboxServer(msg)) return;
  if (! utils.isStaff(msg.member)) return;
  if (msg.author.bot) return;
  if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) return;

  const thread = await threads.findByChannelId(msg.channel.id);
  if (! thread) return;

  if (config.alwaysReply) {
    // AUTO-REPLY: If config.alwaysReply is enabled, send all chat messages in thread channels as replies
    if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
    await thread.replyToUser(msg.member, msg.content.trim(), msg.attachments, config.alwaysReplyAnon || false);
    msg.delete();
  } else {
    // Otherwise just save the messages as "chat" in the logs
    thread.saveChatMessage(msg);
  }
});

/**
 * When we get a private message...
 * 1) Find the open modmail thread for this user, or create a new one
 * 2) Post the message as a user reply in the thread
 */
bot.on('messageCreate', async msg => {
  if (! (msg.channel instanceof Eris.PrivateChannel)) return;
  if (msg.author.bot) return;
  if (msg.type !== 0) return; // Ignore pins etc.

  if (await blocked.isBlocked(msg.author.id)) return;

  // Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created
  messageQueue.add(async () => {
    let thread = await threads.findOpenThreadByUserId(msg.author.id);

    // New thread
    if (! thread) {
      // Ignore messages that shouldn't usually open new threads, such as "ok", "thanks", etc.
      if (config.ignoreAccidentalThreads && msg.content && ACCIDENTAL_THREAD_MESSAGES.includes(msg.content.trim().toLowerCase())) return;

      thread = await threads.createNewThreadForUser(msg.author);
    }

    await thread.receiveUserReply(msg);
  });
});

/**
 * When a message is edited...
 * 1) If that message was in DMs, and we have a thread open with that user, post the edit as a system message in the thread
 * 2) If that message was moderator chatter in the thread, update the corresponding chat message in the DB
 */
bot.on('messageUpdate', async (msg, oldMessage) => {
  if (! msg || ! msg.author) return;
  if (msg.author.bot) return;
  if (await blocked.isBlocked(msg.author.id)) return;

  // Old message content doesn't persist between bot restarts
  const oldContent = oldMessage && oldMessage.content || '*Unavailable due to bot restart*';
  const newContent = msg.content;

  // Ignore bogus edit events with no changes
  if (newContent.trim() === oldContent.trim()) return;

  // 1) Edit in DMs
  if (msg.channel instanceof Eris.PrivateChannel) {
    const thread = await threads.findOpenThreadByUserId(msg.author.id);
    const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`);

    thread.postSystemMessage(editMessage);
  }

  // 2) Edit in the thread
  else if (utils.messageIsOnInboxServer(msg) && utils.isStaff(msg.member)) {
    const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
    if (! thread) return;

    thread.updateChatMessage(msg);
  }
});

/**
 * When a staff message is deleted in a modmail thread, delete it from the database as well
 */
bot.on('messageDelete', async msg => {
  if (! msg.author) return;
  if (msg.author.bot) return;
  if (! utils.messageIsOnInboxServer(msg)) return;
  if (! utils.isStaff(msg.member)) return;

  const thread = await threads.findOpenThreadByChannelId(msg.channel.id);
  if (! thread) return;

  thread.deleteChatMessage(msg.id);
});

/**
 * When the bot is mentioned on the main server, ping staff in the log channel about it
 */
bot.on('messageCreate', async msg => {
  if (! utils.messageIsOnMainServer(msg)) return;
  if (! msg.mentions.some(user => user.id === bot.user.id)) return;

  // If the person who mentioned the modmail bot is also on the modmail server, ignore them
  if (utils.getInboxGuild().members.get(msg.author.id)) return;

  // If the person who mentioned the bot is blocked, ignore them
  if (await blocked.isBlocked(msg.author.id)) return;

  bot.createMessage(utils.getLogChannel(bot).id, {
    content: `@here Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}**: "${msg.cleanContent}"`,
    disableEveryone: false,
  });
});

// Mods can reply to modmail threads using !r or !reply
// These messages get relayed back to the DM thread between the bot and the user
addInboxServerCommand('reply', async (msg, args, thread) => {
  if (! thread) return;

  const text = args.join(' ').trim();
  if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
  await thread.replyToUser(msg.member, text, msg.attachments, false);
  msg.delete();
});

bot.registerCommandAlias('r', 'reply');

// Anonymous replies only show the role, not the username
addInboxServerCommand('anonreply', async (msg, args, thread) => {
  if (! thread) return;

  const text = args.join(' ').trim();
  if (msg.attachments.length) await attachments.saveAttachmentsInMessage(msg);
  await thread.replyToUser(msg.member, text, msg.attachments, true);
  msg.delete();
});

bot.registerCommandAlias('ar', 'anonreply');

// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel.
addInboxServerCommand('close', async (msg, args, thread) => {
  if (! thread) return;
  await thread.close();

  const logUrl = await thread.getLogUrl();
  utils.postLog(utils.trimAll(`
    Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${msg.author.username}
    Logs: ${logUrl}
  `));
});

addInboxServerCommand('block', (msg, args, thread) => {
  async function block(userId) {
    await blocked.block(userId);
    msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`);
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    block(userId);
  } else if (thread) {
    // Calling !block without args in a modmail thread blocks the user of that thread
    block(thread.user_id);
  }
});

addInboxServerCommand('unblock', (msg, args, thread) => {
  async function unblock(userId) {
    await blocked.unblock(userId);
    msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`);
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    unblock(userId);
  } else if (thread) {
    // Calling !unblock without args in a modmail thread unblocks the user of that thread
    unblock(thread.user_id);
  }
});

addInboxServerCommand('logs', (msg, args, thread) => {
  async function getLogs(userId) {
    const userThreads = await threads.getClosedThreadsByUserId(userId);

    // Descending by date
    userThreads.sort((a, b) => {
      if (a.created_at > b.created_at) return -1;
      if (a.created_at < b.created_at) return 1;
      return 0;
    });

    const threadLines = await Promise.all(userThreads.map(async thread => {
      const logUrl = await thread.getLogUrl();
      const formattedDate = moment.utc(thread.created_at).format('MMM Do [at] HH:mm [UTC]');
      return `\`${formattedDate}\`: <${logUrl}>`;
    }));

    const message = `**Log files for <@${userId}>:**\n${threadLines.join('\n')}`;

    // Send the list of logs in chunks of 15 lines per message
    const lines = message.split('\n');
    const chunks = utils.chunk(lines, 15);

    let root = Promise.resolve();
    chunks.forEach(lines => {
      root = root.then(() => msg.channel.createMessage(lines.join('\n')));
    });
  }

  if (args.length > 0) {
    // User mention/id as argument
    const userId = utils.getUserMention(args.join(' '));
    if (! userId) return;
    getLogs(userId);
  } else if (thread) {
    // Calling !logs without args in a modmail thread returns the logs of the user of that thread
    getLogs(thread.user_id);
  }
});

addInboxServerCommand('move', async (msg, args, thread) => {
  if (! config.allowMove) return;

  if (! thread) return;

  const searchStr = args[0];
  if (! searchStr || searchStr.trim() === '') return;

  const normalizedSearchStr = transliterate.slugify(searchStr);

  const categories = msg.channel.guild.channels.filter(c => {
    // Filter to categories that are not the thread's current parent category
    return (c instanceof Eris.CategoryChannel) && (c.id !== msg.channel.parentID);
  });

  if (categories.length === 0) return;

  // See if any category name contains a part of the search string
  const containsRankings = categories.map(cat => {
    const normalizedCatName = transliterate.slugify(cat.name);

    let i;
    for (i = 1; i < normalizedSearchStr.length; i++) {
      if (! normalizedCatName.includes(normalizedSearchStr.slice(0, i))) {
        i--;
        break;
      }
    }

    return [cat, i];
  });

  // Sort by best match
  containsRankings.sort((a, b) => {
    return a[1] > b[1] ? -1 : 1;
  });

  if (containsRankings[0][1] === 0) {
    thread.postNonLogMessage('No matching category');
    return;
  }

  const targetCategory = containsRankings[0][0];

  await bot.editChannel(thread.channel_id, {
    parentID: targetCategory.id
  });

  thread.postSystemMessage(`Thread moved to ${targetCategory.name.toUpperCase()}`);
});

addInboxServerCommand('loglink', async (msg, args, thread) => {
  if (! thread) return;
  const logUrl = await thread.getLogUrl();
  thread.postNonLogMessage(`Log URL: ${logUrl}`);
});

module.exports = {
  async start() {
    // Load plugins
    console.log('Loading plugins...');
    await snippets(bot);
    await greeting(bot);
    await webserver(bot);

    console.log('Connecting to Discord...');
    await bot.connect();

    console.log('Done! Now listening to DMs.');
  }
};
<|endoftext|>


<commit_before>'use strict';

var mongodb = require('mongodb');
var Promise = require('bluebird');
var request = require('request');
var MongoClient = mongodb.MongoClient;
var Collection = mongodb.Collection;

Promise.promisifyAll(MongoClient);
Promise.promisifyAll(Collection.prototype);
Promise.promisifyAll(request);

module.exports = function (context, callback) {
  var mongoUrl = context.data.MONGO_URL;
  var gitHubToken = context.data.GITHUB_TOKEN;
  var body = context.body;
  var action = body.action;
  var repository = body.repository.full_name;
  var issue = body.issue;
  var user = body.comment && body.comment.user.login;
  var comment = body.comment && body.comment.body;
  var db;

  new Promise.resolve()
    .then(connect)
    .then(function () {
      if (action === 'opened') {
        return insertIssue({ issue: issue.number, repository: repository, body: issue.body });
      }
      else if (action === 'created' && /^(\+1)/.test(comment)) {
        return addUser({ issue: issue.number, repository: repository, user: user });
      }
      else if (action === 'created' && /^(-1)/.test(comment)) {
        return removeUser({ issue: issue.number, repository: repository, user: user });
      }
      else {
        throw new Error('invalid payload');
      }
    })
    .then(function () {
      db.close();
      callback();
    })
    .catch(function (err) {
      callback(err);
    });

  function connect() {
    return MongoClient.connectAsync(mongoUrl)
      .then(function (_db) {
        db = _db;
      });
  }

  function insertIssue(obj) {
    return db
      .collection('issues')
      .insertAsync(
        { issue: obj.issue, repository: obj.repository, body: obj.body, users: [] }
      );
  }

  function addUser(obj) {
    return db
      .collection('issues')
      .updateAsync(
        { issue: obj.issue, repository: obj.repository },
        { '$addToSet': { users: obj.user } }
      ).then(function () {
        return updateBody(obj);
      });

  }

  function removeUser(obj) {
    return db
      .collection('issues')
      .updateAsync(
        { issue: obj.issue, repository: obj.repository },
        { '$unset': { users: obj.user } }
      ).then(function () {
        return updateBody(obj);
      });
  }

  function updateBody(obj) {
    return db
      .collection('issues')
      .findOneAsync(
        { issue: obj.issue, repository: obj.repository }
      ).then(function (dbObj) {
        var usersList = (dbObj.users || [])
          .map(function (user) { return '- @' + user })
          .join('\n');
        var usersLength = (dbObj.users || []).length;

        requestAsync(
          {
            url: 'https://api.github.com/repos/' + obj.repository + '/issues/' + obj.issue,
            headers: {
              'Content-type': 'application/json',
              'Authorization': 'token ' + gitHubToken,
              'User-Agent': 'GH-List-App'
            },
            method: 'PATCH',
            json: {
              body: dbObj.body + '\n\n' + usersLength + ' *user' + (usersLength > 1 ? 's' : '') + '*:\n' + usersList
            }
          }
        );
      });
  }

  function requestAsync(payload) {
    return new Promise(function (resolve, reject) {
      request(payload, function (err, response) {
        if (err) {
          reject(err);
          return;
        }

        resolve();
      });
    });
  }
};
<commit_msg>dont return an error if its a hook<commit_after>'use strict';

var mongodb = require('mongodb');
var Promise = require('bluebird');
var request = require('request');
var MongoClient = mongodb.MongoClient;
var Collection = mongodb.Collection;

Promise.promisifyAll(MongoClient);
Promise.promisifyAll(Collection.prototype);
Promise.promisifyAll(request);

module.exports = function (context, callback) {
  var mongoUrl = context.data.MONGO_URL;
  var gitHubToken = context.data.GITHUB_TOKEN;
  var body = context.body;
  var action = body.action;
  var repository = body.repository.full_name;
  var issue = body.issue;
  var user = body.comment && body.comment.user.login;
  var comment = body.comment && body.comment.body;
  var db;

  new Promise.resolve()
    .then(connect)
    .then(function () {
      if (action === 'opened') {
        return insertIssue({ issue: issue.number, repository: repository, body: issue.body });
      }
      else if (action === 'created' && /^(\+1)/.test(comment)) {
        return addUser({ issue: issue.number, repository: repository, user: user });
      }
      else if (action === 'created' && /^(-1)/.test(comment)) {
        return removeUser({ issue: issue.number, repository: repository, user: user });
      }
      else if (body.hook) {
        return;
      }
      else {
        throw new Error('invalid payload');
      }
    })
    .then(function () {
      db.close();
      callback();
    })
    .catch(function (err) {
      callback(err);
    });

  function connect() {
    return MongoClient.connectAsync(mongoUrl)
      .then(function (_db) {
        db = _db;
      });
  }

  function insertIssue(obj) {
    return db
      .collection('issues')
      .insertAsync(
        { issue: obj.issue, repository: obj.repository, body: obj.body, users: [] }
      );
  }

  function addUser(obj) {
    return db
      .collection('issues')
      .updateAsync(
        { issue: obj.issue, repository: obj.repository },
        { '$addToSet': { users: obj.user } }
      ).then(function () {
        return updateBody(obj);
      });

  }

  function removeUser(obj) {
    return db
      .collection('issues')
      .updateAsync(
        { issue: obj.issue, repository: obj.repository },
        { '$unset': { users: obj.user } }
      ).then(function () {
        return updateBody(obj);
      });
  }

  function updateBody(obj) {
    return db
      .collection('issues')
      .findOneAsync(
        { issue: obj.issue, repository: obj.repository }
      ).then(function (dbObj) {
        var usersList = (dbObj.users || [])
          .map(function (user) { return '- @' + user })
          .join('\n');
        var usersLength = (dbObj.users || []).length;

        requestAsync(
          {
            url: 'https://api.github.com/repos/' + obj.repository + '/issues/' + obj.issue,
            headers: {
              'Content-type': 'application/json',
              'Authorization': 'token ' + gitHubToken,
              'User-Agent': 'GH-List-App'
            },
            method: 'PATCH',
            json: {
              body: dbObj.body + '\n\n' + usersLength + ' *user' + (usersLength > 1 ? 's' : '') + '*:\n' + usersList
            }
          }
        );
      });
  }

  function requestAsync(payload) {
    return new Promise(function (resolve, reject) {
      request(payload, function (err, response) {
        if (err) {
          reject(err);
          return;
        }

        resolve();
      });
    });
  }
};
<|endoftext|>


<commit_before>var XHR = (function(){
      var proto = global.XMLHttpRequest.prototype,
          methods = ['open', 'send'],
          originalXHR = {};

      originalXHR.proto = proto;

      if(!methods.length) {
        methods.push('open');
      }

      methods.forEach(function (method) {
        originalXHR[method] = proto[method];
      });

      return originalXHR;
    }()),

    requests = {};

/**
 * @constructor
 * @public
 *
 * @description
 * If there is a callback, it will automatically call
 * the 'done' callback
 *
 * @param {String} path - Path that must be tracked
 * @param {String} [callback] - Done Callback to that path
 *
 */
function XhrListener(path, callback) {
  if(!(this instanceof XhrListener)) {
    var o = Object.create(XhrListener.prototype);
    o.constructor.apply(o, arguments);
    return o;
  }

  this.path = path;

  if(callback) {
    this.done(path, callback);
  }
}

/**
 * @public
 *
 * @description
 * Prototype class of xhrListener.
 * It has all methods that can be accessed through XhrListenerInstance.[method]
 *
 */
XhrListener.prototype = {
  constructor: XhrListener,

 /**
 * @public
 *
 * @description
 * This method will be called when some xhr connection is open
 *
 * @param {String} path - Path that must be tracked
 * @param {String} [callback] - OnOpen Callback to that path
 */
  onopen: function (path, callback) {
    return addRequest.call(this, 'onopen', path, callback);
  },

/**
 * @public
 *
 * @description
 * This method will be called when some xhr connection is done
 *
 * @param {String} path - Path that must be tracked
 * @param {String} [callback] - Done Callback to that path
 */
  done: function (path, callback) {
    return addRequest.call(this, 'done', path, callback);
  },

/**
 * @public
 *
 * @description
 * This method is called when some error occurs
 *
 * @param {String} path - Path that must be tracked
 * @param {String} [callback] - Done Callback to that path
 */
  error: function (path, callback) {
    return addRequest.call(this, 'error', path, callback);
  },

/**
 * @public
 *
 * @description
 * Call the done or error callback before the original handler
 *
 */
  before: function () {
    if(requests[this.path]) {
      requests[this.path].before = true;
    }
    return this;
  },

/**
 * @public
 *
 * @description
 * Call the done or error callback after the original handler
 *
 */
  after: function () {
    if(requests[this.path]) {
      requests[this.path].after = true;
    }
    return this;
  }
};

/**
 * @public
 *
 * @description
 * Gets all tracked requests
 *
 * @return {Object} - Requests' object
 */
XhrListener.getRequestsList = function () {
  return requests;
};

/**
 * @private
 *
 * @description
 * Adds a new request.
 * It will insert a new object into 'requests' containing its
 * path, callback, and type
 *
 * @param {String} type - Type of this request, done, error, onopen..
 * @param {String|Function} [path] - This param is optional unless there is no path defined yet
 * @param {Function} callback - The callback to this request
 *
 */
function addRequest() {
  var args = getArguments(arguments),
      type = args[0],
      path = args[1],
      callback = args[2],
      objectRequest = {};

  //The type param must be a string
  if(!type || typeof type === 'function') {
    console.error('the type of request must be a string');
    return false;
  }

  //The 'path' param might be a function unless the
  //'path' param has already been defined in the
  //current xhrListener's instance
  if(typeof path === 'function') {
    if(typeof this.path === 'undefined') {
      console.error('please define a path to track');
      return false;
    }

    //There is 'path' param in the current xhrListener's instance
    callback = path;
    path = this.path;
  } else {
    //There is no 'path' param, creates a new one
    this.path = path;
  }

  //The 'path' is required, it must be defined
  if(typeof this.path !== 'string' && !(this.path instanceof RegExp)) {
    console.error('there is no path to track');
    return false;
  }

  //Creates a new object request
  objectRequest[path] = requests[path] || {};
  objectRequest[path].path = path;
  objectRequest[path][type] = objectRequest[path][type] || [];
  objectRequest[path][type].push(callback);

  //Attaches this object to the requests object
  requests[path] = objectRequest[path];

  return this;
}

/**
 * @public
 *
 * @description
 * The Open's replaces
 *
 * @param {String} method - The method of this request
 * @param {String} url - The url of this request
 *
 * @return {Object} - An xhr open instance
 */
XHR.proto.open = function (method, url) {
  var _xhr = this, //Save a reference to xhr
      args = arguments, //All arguments

      //The original handlers, it means the callback of
      //xhr.onload or xhr.onreadystatechange
      //that the user has defined
      originalHandlers = {
        onreadystatechange: false
      };

  //Saves a reference to url
  _xhr.url = url;

  //If this request doesn't have this url
  //Don't do anything with this request
  if(!requestFound(_xhr.url)) {
    return XHR.open.apply(this, arguments);
  }

  //Gets url params
  _xhr.params =  {
    get: getUrlParams(url),
    post: {}
  };

  //The xhr method
  _xhr.method = method.toLowerCase();

  //The handler onreadystatechange has already been defined
  if(_xhr.onreadystatechange) {
    originalHandlers.onreadystatechange = _xhr.onreadystatechange;
  }

  //On ready state change handler
  _xhr.onreadystatechange = function () {
    var stateChangeContext = this,
        args = arguments;

    //Loop through each request found, and call the handler
    eachRequestFound(function(path, request) {

      //Call readyState handler before original onreadystatechange
      //By default the handler will be called before original handler
      if(request.before || !request.after) {
        readyStateChangeHandler.apply(stateChangeContext, args);
      }
    }, url);

    //If the user has changed the onreadystatechange, call it
    if(originalHandlers.onreadystatechange) {
      originalHandlers.onreadystatechange.apply(stateChangeContext, arguments);

      //Loop through each request found, and call handler
      eachRequestFound(function(path, request) {
        if(request.after) {
          readyStateChangeHandler.apply(stateChangeContext, args);
        }
      }, url);
    }
  };

  //Observes the changes on onreadystatechange
  observePropertyChange.call(_xhr, 'onreadystatechange', function (fn) {
    originalHandlers.onreadystatechange = fn;
  });

  //Loop through each request found, call the onopen callback if it exists
  eachRequestFound(function(path, request) {
    if(request.onopen) {
      request.onopen.forEach(function (onopen) {
        onopen.apply(_xhr, args);
      });
    }
  }, url);

  return XHR.open.apply(this, arguments);
};

/**
 * @public
 *
 * @description
 * The Send's replaces
 *
 * @return {Object} - An xhr send instance
 */
XHR.proto.send = function (params) {
  var _xhr = this;

  if(!requestFound(_xhr.url)) {
    return XHR.send.apply(_xhr, arguments);
  }

  _xhr.params.post = params ? getUrlParams(params) : '';
  return XHR.send.apply(_xhr, arguments);
};

/**
 * @private
 *
 * @description
 * Handler to onload request
 *
 */
function onloadHandler() {
  var ready = this,
      args = arguments,
      successStatus = ready.status >= 200 && ready.status < 300 || ready.status === 304;

  eachRequestFound(function(path, request) {

    //The request is ok and there is an done callback
    if(request.done && successStatus) {
      var type = getType(ready.getResponseHeader('Content-Type'));

      //Defines a object containing the type of this request
      ready.is = isType(type);

      //Defines the support of response overwrite
      ready.responseOverwrite = canIRewriteResponse.call(ready);

      //Calls each on 'done' callback
      //This loop is necessary because we can define more than one
      //callback for the same path, like using a global
      //path '/.*/' for every request
      request.done.forEach(function (done) {
        done.call(ready, ready.responseText, ready.response, type);

        //If the response can't be overwritten
        if(!ready.responseOverwrite) {
          return;
        }

        //Sets a new responseText if it exists
        if(ready.newResponseText) {
          defineNewResponse.call(ready, 'responseText');
        }

        //Sets a new response if it exists
        if(ready.newResponse) {
          defineNewResponse.call(ready, 'response');
        }
      });
    }

    //There are errors and ready.status is different than 200,
    //call error callback
    else if(request.error && !successStatus) {
      request.error.forEach(function (error) {
        error.call(ready, ready.status, ready.statusText);
      });
    }
  }, ready.url);
}

/**
 * @private
 *
 * @description
 * Handler to ready state change
 *
 */
function readyStateChangeHandler() {
  var ready = this;

  if(ready.readyState === 4) {
    onloadHandler.apply(ready, arguments);
  }
}

/**
 * @private
 *
 * @description
 * Call eachRequestFound with the url param
 *
 * @param {String} url - url to test
 *
 */
function requestFound(url) {
  return eachRequestFound(false, url);
}

/**
 * @private
 *
 * @description
 * Matches the url param with tracked urls
 *
 * @param {Function} callback - Callback to request found
 * @param {String} url - Url that needs to be compared with tracked urls
 */
function eachRequestFound(callback, url) {
  var found = false;

  eachRequest(function(request) {
    if(matchPath(request.path, url)) {
      if(callback) {
        callback(request.path, request);
      }

      found = true;
    }
  });

  return found;
}

/**
 * @private
 *
 * @description
 * Call the callback for each requests
 *
 * @param {Function} callback - Callback to request found
 *
 */
function eachRequest(callback) {
  var key;

  for(key in requests) {
    callback(requests[key]);
  }
}

/**
 * @private
 *
 * @description
 * Matches the path param with the tracked url
 *
 * @param {String} path - Original url of request
 * @param {String} url - Url that must be tracked
 *
 */
function matchPath(path, url) {
  return typeof path !== 'string' ? path.test(url) : path === url;
}

/**
 * @private
 *
 * @description
 * Simples alias for Array.prototype.slice.call
 *
 * @param {ArrayLike} args - Array-Like of Arguments
 *
 */
function getArguments(args) {
  return Array.prototype.slice.call(args);
}

/**
 * @private
 *
 * @description
 * Check if the the response might be overwritten
 *
 * @return {Boolean} returns true whether response's overwriting is supported
 */
function canIRewriteResponse() {
  try {
    Object.defineProperty(this, 'responseText', {
      configurable: true
    });
    return true;
  } catch(e) {
    return false;
  }
}

/**
 * @private
 *
 * @description
 * Allows to replace the 'response' property for a new value
 *
 * @param {String} responseType - The type of response, it should be 'responseText' or 'response'
 *
 */
function defineNewResponse(responseType) {
  var property = responseType,
      newProperty = 'new' + property[0].toUpperCase() + property.slice(1),
      val = this[property];

  try {
    Object.defineProperty(this, property, {
      get: function() {
        return val;
      },

      set: function() {
        val = this[newProperty];
        return val;
      },

      configurable: true
    });

    this[property] = this[newProperty];
  } catch(e) {
    return false;
  }
}

/**
 * @private
 *
 * @description
 * Observer for property change, when something is changed on object,
 * the callback is called
 *
 * @param {String} property - Property that has to be observed
 * @param {Function} callback - Callback to this observer
 *
 */
function observePropertyChange(property, callback) {
  var val = this[property];

  try {
    Object.defineProperty(this, property, {
      get: function() {
        return val;
      },

      set: function(newVal) {
        val = newVal;
        callback.call(this, val);
        return val;
      },

      configurable: true,
      writable: true
    });
  } catch (e) {
    return false;
  }
}

/**
 * @private
 *
 * @description
 * Matches the type of request
 *
 * @param {String} type - Content type of request
 * @returns {String} - Content type of request
 */
function getType(type) {
  if (type.match('json')) {
    return 'json';
  }

  if (type.match('javascript')) {
    return 'js';
  }

  if (type.match('text')) {
    return 'html';
  }

  if (type.match('xml')) {
    return 'xml';
  }

  return 'text';
}

/**
 * @private
 *
 * @description
 * Matches type of request
 *
 * @param {String} type - Type of request
 *
 * @return {Object} - Object containing the types and if it matches with the type param
 */
function isType(type) {
  return {
    html: type === 'html',
    json: type === 'json',
    js: type === 'js',
    text: type === 'text'
  };
}

/**
 * @private
 *
 * @description
 * Gets the url params
 *
 * @param {String} url - Url of request
 *
 * @return {Object} - Object containing the params
 */
function getUrlParams(url) {
  var params = {},
      hasParams = false;

  url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (match, key, value) {
    params[key] = value;
    hasParams = true;
  });

  return hasParams ? params : '';
}

//Version.
XhrListener.VERSION = '0.0.1';

//Export to the global
global.XhrListener = XhrListener;
<commit_msg>adding new .after and .before feature for all methods<commit_after>var XHR = (function(){
      var proto = global.XMLHttpRequest.prototype,
          methods = ['open', 'send'],
          originalXHR = {};

      originalXHR.proto = proto;

      if(!methods.length) {
        methods.push('open');
      }

      methods.forEach(function (method) {
        originalXHR[method] = proto[method];
      });

      return originalXHR;
    }()),

    requests = {};

/**
 * @constructor
 * @public
 *
 * @description
 * If there is a callback, it will automatically call
 * the 'done' callback
 *
 * @param {String} path - Path that must be tracked
 * @param {String} [callback] - Done Callback to that path
 *
 */
function XhrListener(path, callback) {
  if(!(this instanceof XhrListener)) {
    var o = Object.create(XhrListener.prototype);
    o.constructor.apply(o, arguments);
    return o;
  }

  this.path = path;

  if(callback) {
    this.done(path, callback);
  }
}

/**
 * @public
 *
 * @description
 * Prototype class of xhrListener.
 * It has all methods that can be accessed through XhrListenerInstance.[method]
 *
 */
XhrListener.prototype = {
  constructor: XhrListener,

 /**
 * @public
 *
 * @description
 * This method will be called when some xhr connection is open
 *
 * @param {String} path - Path that must be tracked
 * @param {String} [callback] - OnOpen Callback to that path
 */
  onopen: function (path, callback) {
    return addRequest.call(this, 'onopen', path, callback);
  },

/**
 * @public
 *
 * @description
 * This method will be called when some xhr connection is done
 *
 * @param {String} path - Path that must be tracked
 * @param {String} [callback] - Done Callback to that path
 */
  done: function (path, callback) {
    return addRequest.call(this, 'done', path, callback);
  },

/**
 * @public
 *
 * @description
 * This method is called when some error occurs
 *
 * @param {String} path - Path that must be tracked
 * @param {String} [callback] - Done Callback to that path
 */
  error: function (path, callback) {
    return addRequest.call(this, 'error', path, callback);
  },

/**
 * @public
 *
 * @description
 * Call the callback before the original handler
 *
 */
  before: function () { },

/**
 * @public
 *
 * @description
 * Call the callback after the original handler
 *
 */
  after: function () { }
};

/**
 * @public
 *
 * @description
 * Gets all tracked requests
 *
 * @return {Object} - Requests' object
 */
XhrListener.getRequestsList = function () {
  return requests;
};

/**
 * @public
 *
 * @description
 * Clear all tracked requests
 *
 */
XhrListener.clearRequestsList = function () {
  requests = {};
};

/**
 * @private
 *
 * @description
 * Adds a new request.
 * It will insert a new object into 'requests' containing its
 * path, callback, and type
 *
 * @param {String} type - Type of this request, done, error, onopen..
 * @param {String|Function} [path] - This param is optional unless there is no path defined yet
 * @param {Function} callback - The callback to this request
 *
 */
function addRequest() {
  var instance = this,
      args = getArguments(arguments),
      type = args[0],
      path = args[1],
      callback = args[2],
      objectRequest = {};

  //The type param must be a string
  if(!type || typeof type === 'function') {
    console.error('the type of request must be a string');
    return false;
  }

  //The 'path' param might be a function unless the
  //'path' param has already been defined in the
  //current xhrListener's instance
  if(typeof path === 'function') {
    if(typeof this.path === 'undefined') {
      console.error('please define a path to track');
      return false;
    }

    //There is 'path' param in the current xhrListener's instance
    callback = path;
    path = this.path;
  } else {
    //There is no 'path' param, creates a new one
    this.path = path;
  }

  //The 'path' is required, it must be defined
  if(typeof this.path !== 'string' && !(this.path instanceof RegExp)) {
    console.error('there is no path to track');
    return false;
  }

  //Order of order to call this callback, after or before original handler
  callback.callOrder = 'before';

  //Creates a new object request
  objectRequest[path] = requests[path] || {};
  objectRequest[path].path = path;
  objectRequest[path][type] = objectRequest[path][type] || [];
  objectRequest[path][type].push(callback);

  //Attaches this object to the requests object
  requests[path] = objectRequest[path];

  //Define the methods before and after
  function MainRequestObject() {
    instance.before = this.before = function() {
      callback.callOrder = 'before';
      return this;
    };

    instance.after = this.after = function() {
      callback.callOrder = 'after';
      return this;
    };
  }

  //Attaches the current instance to the prototype of
  //the main request object
  MainRequestObject.prototype = instance;

  return new MainRequestObject();
}

/**
 * @public
 *
 * @description
 * The Open's replaces
 *
 * @param {String} method - The method of this request
 * @param {String} url - The url of this request
 *
 * @return {Object} - An xhr open instance
 */
XHR.proto.open = function (method, url) {
  var _xhr = this, //Save a reference to xhr
      args = arguments, //All arguments

      //The original handlers, it means the callback of
      //xhr.onload or xhr.onreadystatechange
      //that the user has defined
      originalHandlers = {
        onreadystatechange: false
      };

  //Saves a reference to url
  _xhr.url = url;

  //If this request doesn't have this url
  //Don't do anything with this request
  if(!requestFound(_xhr.url)) {
    return XHR.open.apply(this, arguments);
  }

  //Gets url params
  _xhr.params =  {
    get: getUrlParams(url),
    post: {}
  };

  //The xhr method
  _xhr.method = method.toLowerCase();

  //The handler onreadystatechange has already been defined
  if(_xhr.onreadystatechange) {
    originalHandlers.onreadystatechange = _xhr.onreadystatechange;
  }

  //On ready state change handler
  _xhr.onreadystatechange = function () {
    var stateChangeContext = this,
        args = arguments,

        callback = function (request) {
          readyStateChangeHandler.call(stateChangeContext, request);
        },

        originalHandler = function () {
          if(originalHandlers.onreadystatechange) {
            originalHandlers.onreadystatechange.apply(stateChangeContext, args);
          }
        };

    callHandler(['done', 'error'], callback, originalHandler, url);
  };

  //Observes the changes on onreadystatechange
  observePropertyChange.call(_xhr, 'onreadystatechange', function (fn) {
    originalHandlers.onreadystatechange = fn;
  });

  //Loop through each request found, call the onopen callback if it exists
  return callHandler(['onopen'],

    //Callback
    function (request, openCallback) {
      openCallback.apply(_xhr, args);
    },

    //Original handler
    function () {
      return XHR.open.apply(_xhr, args);

    }, url);
};

/**
 * @public
 *
 * @description
 * The Send's replaces
 *
 * @return {Object} - An xhr send instance
 */
XHR.proto.send = function (params) {
  var _xhr = this;

  if(!requestFound(_xhr.url)) {
    return XHR.send.apply(_xhr, arguments);
  }

  _xhr.params.post = params ? getUrlParams(params) : '';
  return XHR.send.apply(_xhr, arguments);
};

/**
 * @private
 *
 * @description
 * Call handler in a properly order
 *
 */
function callHandler(handlers, callback, originalCallback, url) {
  var types = this,
      returnOriginal = false,
      callbackCreate = function (callback, request, requestsCallbacks) {
        return function() {
          callback(request, requestsCallbacks);
        };
      },

      callBefore = [],
      callAfter = [];

  eachRequestFound(function(path, request) {
    var requestType,
        index = 0,
        requestsCallbacks = [],
        requestsLength = 0;

    for(requestType in request) {

      if(Object.prototype.toString.call(request[requestType]) !== '[object Array]') {
        continue;
      }

      if(!~handlers.indexOf(requestType)) {
        continue;
      }

      requestsCallbacks = request[requestType];
      requestsLength = requestsCallbacks.length;

      for(; index < requestsLength; index++) {
        if(requestsCallbacks[index].callOrder === 'before') {
          callBefore.push(callbackCreate(callback, request, requestsCallbacks[index]));
          return;
        }

        if(requestsCallbacks[index].callOrder === 'after') {
          callAfter.push(callbackCreate(callback, request, requestsCallbacks[index]));
        }
      }
    }
  }, url);

  callBefore.forEach(function (callback) {
    callback();
  });

  returnOriginal = originalCallback();

  callAfter.forEach(function (callback) {
    callback();
  });

  return returnOriginal;
}

/**
 * @private
 *
 * @description
 * Handler to onload request
 *
 */
function onloadHandler(request) {
  var ready = this,
      successStatus = ready.status >= 200 && ready.status < 300 || ready.status === 304;

  //The request is ok and there is an done callback
  if(request.done && successStatus) {
    var type = getType(ready.getResponseHeader('Content-Type'));

    //Defines a object containing the type of this request
    ready.is = isType(type);

    //Defines the support of response overwrite
    ready.responseOverwritable = canIRewriteResponse.call(ready);

    //Calls each on 'done' callback
    //This loop is necessary because we can define more than one
    //callback for the same path, like using a global
    //path '/.*/' for every request
    request.done.forEach(function (done) {
      done.call(ready, ready.responseText, ready.response, type);

      //If the response can't be overwritten
      if(!ready.responseOverwritable) {
        return;
      }

      //Sets a new responseText if it exists
      if(ready.newResponseText) {
        defineNewResponse.call(ready, 'responseText');
      }

      //Sets a new response if it exists
      if(ready.newResponse) {
        defineNewResponse.call(ready, 'response');
      }
    });
  }

  //There are errors and ready.status is different than 200,
  //call error callback
  else if(request.error && !successStatus) {
    request.error.forEach(function (error) {
      error.call(ready, ready.status, ready.statusText);
    });
  }
}

/**
 * @private
 *
 * @description
 * Handler to ready state change
 *
 */
function readyStateChangeHandler(request) {
  var ready = this;

  if(ready.readyState === 4) {
    onloadHandler.call(ready, request);
  }
}

/**
 * @private
 *
 * @description
 * Call eachRequestFound with the url param
 *
 * @param {String} url - url to test
 *
 */
function requestFound(url) {
  return eachRequestFound(false, url);
}

/**
 * @private
 *
 * @description
 * Matches the url param with tracked urls
 *
 * @param {Function} callback - Callback to request found
 * @param {String} url - Url that needs to be compared with tracked urls
 */
function eachRequestFound(callback, url) {
  var found = false;

  eachRequest(function(request) {
    if(matchPath(request.path, url)) {
      if(callback) {
        callback(request.path, request);
      }

      found = true;
    }
  });

  return found;
}

/**
 * @private
 *
 * @description
 * Call the callback for each requests
 *
 * @param {Function} callback - Callback to request found
 *
 */
function eachRequest(callback) {
  var key;

  for(key in requests) {
    callback(requests[key]);
  }
}

/**
 * @private
 *
 * @description
 * Matches the path param with the tracked url
 *
 * @param {String} path - Original url of request
 * @param {String} url - Url that must be tracked
 *
 */
function matchPath(path, url) {
  return typeof path !== 'string' ? path.test(url) : path === url;
}

/**
 * @private
 *
 * @description
 * Simples alias for Array.prototype.slice.call
 *
 * @param {ArrayLike} args - Array-Like of Arguments
 *
 */
function getArguments(args) {
  return Array.prototype.slice.call(args);
}

/**
 * @private
 *
 * @description
 * Check if the the response might be overwritten
 *
 * @return {Boolean} returns true whether response's overwriting is supported
 */
function canIRewriteResponse() {
  try {
    Object.defineProperty(this, 'responseText', {
      configurable: true
    });
    return true;
  } catch(e) {
    return false;
  }
}

/**
 * @private
 *
 * @description
 * Allows to replace the 'response' property for a new value
 *
 * @param {String} responseType - The type of response, it should be 'responseText' or 'response'
 *
 */
function defineNewResponse(responseType) {
  var property = responseType,
      newProperty = 'new' + property[0].toUpperCase() + property.slice(1),
      val = this[property];

  try {
    Object.defineProperty(this, property, {
      get: function() {
        return val;
      },

      set: function() {
        val = this[newProperty];
        return val;
      },

      configurable: true
    });

    this[property] = this[newProperty];
  } catch(e) {
    return false;
  }
}

/**
 * @private
 *
 * @description
 * Observer for property change, when something is changed on object,
 * the callback is called
 *
 * @param {String} property - Property that has to be observed
 * @param {Function} callback - Callback to this observer
 *
 */
function observePropertyChange(property, callback) {
  var val = this[property];

  try {
    Object.defineProperty(this, property, {
      get: function() {
        return val;
      },

      set: function(newVal) {
        val = newVal;
        callback.call(this, val);
        return val;
      },

      configurable: true
    });
  } catch (e) {
    return false;
  }
}

/**
 * @private
 *
 * @description
 * Matches the type of request
 *
 * @param {String} type - Content type of request
 * @returns {String} - Content type of request
 */
function getType(type) {
  if (type.match('json')) {
    return 'json';
  }

  if (type.match('javascript')) {
    return 'js';
  }

  if (type.match('text')) {
    return 'html';
  }

  if (type.match('xml')) {
    return 'xml';
  }

  return 'text';
}

/**
 * @private
 *
 * @description
 * Matches type of request
 *
 * @param {String} type - Type of request
 *
 * @return {Object} - Object containing the types and if it matches with the type param
 */
function isType(type) {
  return {
    html: type === 'html',
    json: type === 'json',
    js: type === 'js',
    text: type === 'text'
  };
}

/**
 * @private
 *
 * @description
 * Gets the url params
 *
 * @param {String} url - Url of request
 *
 * @return {Object} - Object containing the params
 */
function getUrlParams(url) {
  var params = {},
      hasParams = false;

  url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (match, key, value) {
    params[key] = value;
    hasParams = true;
  });

  return hasParams ? params : '';
}

//Version.
XhrListener.VERSION = '0.0.1';

//Export to the global
global.XhrListener = XhrListener;
<|endoftext|>


<commit_before>import path from 'path';
import fs from 'fs-extra';
import React from 'react';
import {shallow} from 'enzyme';

import Commit from '../../lib/models/commit';
import {nullBranch} from '../../lib/models/branch';

import CommitController, {COMMIT_GRAMMAR_SCOPE} from '../../lib/controllers/commit-controller';
import {cloneRepository, buildRepository, buildRepositoryWithPipeline} from '../helpers';

describe('CommitController', function() {
  let atomEnvironment, workspace, commandRegistry, notificationManager, lastCommit, config, confirm, tooltips;
  let app;

  beforeEach(function() {
    atomEnvironment = global.buildAtomEnvironment();
    workspace = atomEnvironment.workspace;
    commandRegistry = atomEnvironment.commands;
    notificationManager = atomEnvironment.notifications;
    config = atomEnvironment.config;
    tooltips = atomEnvironment.tooltips;
    confirm = sinon.stub(atomEnvironment, 'confirm');

    lastCommit = new Commit({sha: 'a1e23fd45', message: 'last commit message'});
    const noop = () => {};

    app = (
      <CommitController
        workspace={workspace}
        grammars={atomEnvironment.grammars}
        commandRegistry={commandRegistry}
        tooltips={tooltips}
        config={config}
        notificationManager={notificationManager}
        repository={{}}
        isMerging={false}
        mergeConflictsExist={false}
        stagedChangesExist={false}
        mergeMessage={''}
        lastCommit={lastCommit}
        currentBranch={nullBranch}
        prepareToCommit={noop}
        commit={noop}
        abortMerge={noop}
      />
    );
  });

  afterEach(function() {
    atomEnvironment.destroy();
  });

  it('correctly updates state when switching repos', async function() {
    const workdirPath1 = await cloneRepository('three-files');
    const repository1 = await buildRepository(workdirPath1);
    const workdirPath2 = await cloneRepository('three-files');
    const repository2 = await buildRepository(workdirPath2);

    app = React.cloneElement(app, {repository: repository1});
    const wrapper = shallow(app, {disableLifecycleMethods: true});

    assert.strictEqual(wrapper.instance().getCommitMessage(), '');

    wrapper.instance().setCommitMessage('message 1');

    wrapper.setProps({repository: repository2});

    assert.strictEqual(wrapper.instance().getCommitMessage(), '');

    wrapper.setProps({repository: repository1});
    assert.equal(wrapper.instance().getCommitMessage(), 'message 1');
  });

  describe('the passed commit message', function() {
    let repository;

    beforeEach(async function() {
      const workdirPath = await cloneRepository('three-files');
      repository = await buildRepository(workdirPath);
      app = React.cloneElement(app, {repository});
    });

    it('is set to the getCommitMessage() in the default case', function() {
      repository.setCommitMessage('some message');
      const wrapper = shallow(app, {disableLifecycleMethods: true});
      assert.strictEqual(wrapper.find('CommitView').prop('message'), 'some message');
    });

    it('repository.didUpdate is not called when commit message changes', function() {
      repository.setCommitMessage('some message');
      const wrapper = shallow(app, {disableLifecycleMethods: true}).instance();
      sinon.spy(wrapper.props.repository.state, 'didUpdate');
      assert.strictEqual(wrapper.getCommitMessage(), 'some message');
      wrapper.handleMessageChange('new message');
      assert.strictEqual(wrapper.getCommitMessage(), 'new message');
      assert.isFalse(wrapper.props.repository.state.didUpdate.called);
    });

    describe('when a merge message is defined', function() {
      it('is set to the merge message when merging', function() {
        app = React.cloneElement(app, {isMerging: true, mergeMessage: 'merge conflict!'});
        const wrapper = shallow(app, {disableLifecycleMethods: true});
        assert.strictEqual(wrapper.find('CommitView').prop('message'), 'merge conflict!');
      });

      it('is set to getCommitMessage() if it is set', function() {
        repository.setCommitMessage('some commit message');
        app = React.cloneElement(app, {isMerging: true, mergeMessage: 'merge conflict!'});
        const wrapper = shallow(app, {disableLifecycleMethods: true});
        assert.strictEqual(wrapper.find('CommitView').prop('message'), 'some commit message');
      });
    });
  });

  describe('committing', function() {
    let workdirPath, repository;

    beforeEach(async function() {
      workdirPath = await cloneRepository('three-files');
      repository = await buildRepositoryWithPipeline(workdirPath, {confirm, notificationManager, workspace});
      const commit = message => repository.commit(message);

      app = React.cloneElement(app, {repository, commit});
    });

    it('clears the commit messages', async function() {
      repository.setCommitMessage('a message');

      await fs.writeFile(path.join(workdirPath, 'a.txt'), 'some changes', {encoding: 'utf8'});
      await repository.git.exec(['add', '.']);

      const wrapper = shallow(app, {disableLifecycleMethods: true});
      await wrapper.instance().commit('another message');

      assert.strictEqual(repository.getCommitMessage(), '');
    });

    it('issues a notification on failure', async function() {
      repository.setCommitMessage('some message');

      sinon.spy(notificationManager, 'addError');

      const wrapper = shallow(app, {disableLifecycleMethods: true});

      // Committing with no staged changes should cause commit error
      try {
        await wrapper.instance().commit('message');
      } catch (e) {
        assert(e, 'is error');
      }

      assert.isTrue(notificationManager.addError.called);

      assert.strictEqual(repository.getCommitMessage(), 'some message');
    });

    describe('message formatting', function() {
      let commitSpy;
      beforeEach(function() {
        commitSpy = sinon.stub().returns(Promise.resolve());
        app = React.cloneElement(app, {commit: commitSpy});
      });

      it('wraps the commit message body at 72 characters if github.automaticCommitMessageWrapping is true', async function() {
        config.set('github.automaticCommitMessageWrapping', false);

        const wrapper = shallow(app, {disableLifecycleMethods: true});

        await wrapper.instance().commit([
          'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor',
          '',
          'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
        ].join('\n'));

        assert.deepEqual(commitSpy.args[0][0].split('\n'), [
          'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor',
          '',
          'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
        ]);

        commitSpy.reset();
        config.set('github.automaticCommitMessageWrapping', true);

        await wrapper.instance().commit([
          'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor',
          '',
          'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
        ].join('\n'));

        assert.deepEqual(commitSpy.args[0][0].split('\n'), [
          'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor',
          '',
          'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ',
          'ut aliquip ex ea commodo consequat.',
        ]);
      });
    });

    describe('toggling between commit box and commit editor', function() {
      it('transfers the commit message contents of the last editor', async function() {
        const wrapper = shallow(app, {disableLifecycleMethods: true});

        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('message in box');
        await assert.async.equal(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath());
        wrapper.update();
        assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox'));

        const editor = workspace.getActiveTextEditor();
        assert.strictEqual(editor.getText(), 'message in box');
        editor.setText('message in editor');
        await editor.save();

        workspace.paneForItem(editor).splitRight({copyActiveItem: true});
        await assert.async.notEqual(workspace.getActiveTextEditor(), editor);

        editor.destroy();
        assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox'));

        workspace.getActiveTextEditor().destroy();
        assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox'));
        await assert.async.strictEqual(wrapper.update().find('CommitView').prop('message'), 'message in editor');
      });

      it('activates editor if already opened but in background', async function() {
        const wrapper = shallow(app, {disableLifecycleMethods: true});

        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('sup');
        await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath());
        const editor = workspace.getActiveTextEditor();

        await workspace.open(path.join(workdirPath, 'a.txt'));
        workspace.getActivePane().splitRight();
        await workspace.open(path.join(workdirPath, 'b.txt'));
        assert.notStrictEqual(workspace.getActiveTextEditor(), editor);

        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')();
        await assert.async.strictEqual(workspace.getActiveTextEditor(), editor);
      });

      it('closes all open commit message editors if one is in the foreground of a pane, prompting for unsaved changes', async function() {
        const wrapper = shallow(app, {disableLifecycleMethods: true});

        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('sup');
        await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath());

        const editor = workspace.getActiveTextEditor();
        workspace.paneForItem(editor).splitRight({copyActiveItem: true});
        assert.lengthOf(wrapper.instance().getCommitMessageEditors(), 2);

        // Activate another editor but keep commit message editor in foreground of inactive pane
        await workspace.open(path.join(workdirPath, 'a.txt'));
        assert.notStrictEqual(workspace.getActiveTextEditor(), editor);

        editor.setText('make some new changes');

        // atom internals calls `confirm` on the ApplicationDelegate instead of the atom environment
        sinon.stub(atomEnvironment.applicationDelegate, 'confirm').callsFake((options, callback) => {
          if (typeof callback === 'function') {
            callback(0); // Save
          }
          return 0; // TODO: Remove this return and typeof check once https://github.com/atom/atom/pull/16229 is on stable
        });
        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')();
        await assert.async.lengthOf(wrapper.instance().getCommitMessageEditors(), 0);
        assert.isTrue(atomEnvironment.applicationDelegate.confirm.called);
        await assert.async.strictEqual(wrapper.update().find('CommitView').prop('message'), 'make some new changes');
      });
    });

    describe('committing from commit editor', function() {
      it('uses git commit grammar in the editor', async function() {
        const wrapper = shallow(app, {disableLifecycleMethods: true});
        await atomEnvironment.packages.activatePackage('language-git');
        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('sup');
        await assert.async.strictEqual(workspace.getActiveTextEditor().getGrammar().scopeName, COMMIT_GRAMMAR_SCOPE);
      });

      it('takes the commit message from the editor and deletes the `ATOM_COMMIT_EDITMSG` file', async function() {
        fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'some changes');
        await repository.stageFiles(['a.txt']);

        app = React.cloneElement(app, {prepareToCommit: () => true, stagedChangesExist: true});
        const wrapper = shallow(app, {disableLifecycleMethods: true});

        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')();
        await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath());

        const editor = workspace.getActiveTextEditor();
        editor.setText('message in editor');
        await editor.save();
        wrapper.find('CommitView').prop('commit')('message in box');

        await assert.async.strictEqual((await repository.getLastCommit()).getMessageSubject(), 'message in editor');
        await assert.async.isFalse(fs.existsSync(wrapper.instance().getCommitMessagePath()));
      });

      it('asks user to confirm if commit editor has unsaved changes', async function() {
        app = React.cloneElement(app, {confirm, prepareToCommit: () => true, stagedChangesExist: true});
        const wrapper = shallow(app, {disableLifecycleMethods: true});

        sinon.stub(repository.git, 'commit');
        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')();
        await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath());

        const editor = workspace.getActiveTextEditor();
        editor.setText('unsaved changes');
        workspace.paneForItem(editor).splitRight({copyActiveItem: true});
        await assert.async.notStrictEqual(workspace.getActiveTextEditor(), editor);
        assert.lengthOf(workspace.getTextEditors(), 2);

        confirm.returns(1); // Cancel
        wrapper.find('CommitView').prop('commit')('message in box');
        assert.strictEqual(repository.git.commit.callCount, 0);

        confirm.returns(0); // Commit
        wrapper.find('CommitView').prop('commit')('message in box');
        await assert.async.equal(repository.git.commit.callCount, 1);
        await assert.async.lengthOf(workspace.getTextEditors(), 0);
      });
    });
  });
});
<commit_msg>update test description<commit_after>import path from 'path';
import fs from 'fs-extra';
import React from 'react';
import {shallow} from 'enzyme';

import Commit from '../../lib/models/commit';
import {nullBranch} from '../../lib/models/branch';

import CommitController, {COMMIT_GRAMMAR_SCOPE} from '../../lib/controllers/commit-controller';
import {cloneRepository, buildRepository, buildRepositoryWithPipeline} from '../helpers';

describe('CommitController', function() {
  let atomEnvironment, workspace, commandRegistry, notificationManager, lastCommit, config, confirm, tooltips;
  let app;

  beforeEach(function() {
    atomEnvironment = global.buildAtomEnvironment();
    workspace = atomEnvironment.workspace;
    commandRegistry = atomEnvironment.commands;
    notificationManager = atomEnvironment.notifications;
    config = atomEnvironment.config;
    tooltips = atomEnvironment.tooltips;
    confirm = sinon.stub(atomEnvironment, 'confirm');

    lastCommit = new Commit({sha: 'a1e23fd45', message: 'last commit message'});
    const noop = () => {};

    app = (
      <CommitController
        workspace={workspace}
        grammars={atomEnvironment.grammars}
        commandRegistry={commandRegistry}
        tooltips={tooltips}
        config={config}
        notificationManager={notificationManager}
        repository={{}}
        isMerging={false}
        mergeConflictsExist={false}
        stagedChangesExist={false}
        mergeMessage={''}
        lastCommit={lastCommit}
        currentBranch={nullBranch}
        prepareToCommit={noop}
        commit={noop}
        abortMerge={noop}
      />
    );
  });

  afterEach(function() {
    atomEnvironment.destroy();
  });

  it('correctly updates state when switching repos', async function() {
    const workdirPath1 = await cloneRepository('three-files');
    const repository1 = await buildRepository(workdirPath1);
    const workdirPath2 = await cloneRepository('three-files');
    const repository2 = await buildRepository(workdirPath2);

    app = React.cloneElement(app, {repository: repository1});
    const wrapper = shallow(app, {disableLifecycleMethods: true});

    assert.strictEqual(wrapper.instance().getCommitMessage(), '');

    wrapper.instance().setCommitMessage('message 1');

    wrapper.setProps({repository: repository2});

    assert.strictEqual(wrapper.instance().getCommitMessage(), '');

    wrapper.setProps({repository: repository1});
    assert.equal(wrapper.instance().getCommitMessage(), 'message 1');
  });

  describe('the passed commit message', function() {
    let repository;

    beforeEach(async function() {
      const workdirPath = await cloneRepository('three-files');
      repository = await buildRepository(workdirPath);
      app = React.cloneElement(app, {repository});
    });

    it('is set to the getCommitMessage() in the default case', function() {
      repository.setCommitMessage('some message');
      const wrapper = shallow(app, {disableLifecycleMethods: true});
      assert.strictEqual(wrapper.find('CommitView').prop('message'), 'some message');
    });

    it('changing commit message does not cause the repository to update', function() {
      repository.setCommitMessage('some message');
      const wrapper = shallow(app, {disableLifecycleMethods: true}).instance();
      sinon.spy(wrapper.props.repository.state, 'didUpdate');
      assert.strictEqual(wrapper.getCommitMessage(), 'some message');
      wrapper.handleMessageChange('new message');
      assert.strictEqual(wrapper.getCommitMessage(), 'new message');
      assert.isFalse(wrapper.props.repository.state.didUpdate.called);
    });

    describe('when a merge message is defined', function() {
      it('is set to the merge message when merging', function() {
        app = React.cloneElement(app, {isMerging: true, mergeMessage: 'merge conflict!'});
        const wrapper = shallow(app, {disableLifecycleMethods: true});
        assert.strictEqual(wrapper.find('CommitView').prop('message'), 'merge conflict!');
      });

      it('is set to getCommitMessage() if it is set', function() {
        repository.setCommitMessage('some commit message');
        app = React.cloneElement(app, {isMerging: true, mergeMessage: 'merge conflict!'});
        const wrapper = shallow(app, {disableLifecycleMethods: true});
        assert.strictEqual(wrapper.find('CommitView').prop('message'), 'some commit message');
      });
    });
  });

  describe('committing', function() {
    let workdirPath, repository;

    beforeEach(async function() {
      workdirPath = await cloneRepository('three-files');
      repository = await buildRepositoryWithPipeline(workdirPath, {confirm, notificationManager, workspace});
      const commit = message => repository.commit(message);

      app = React.cloneElement(app, {repository, commit});
    });

    it('clears the commit messages', async function() {
      repository.setCommitMessage('a message');

      await fs.writeFile(path.join(workdirPath, 'a.txt'), 'some changes', {encoding: 'utf8'});
      await repository.git.exec(['add', '.']);

      const wrapper = shallow(app, {disableLifecycleMethods: true});
      await wrapper.instance().commit('another message');

      assert.strictEqual(repository.getCommitMessage(), '');
    });

    it('issues a notification on failure', async function() {
      repository.setCommitMessage('some message');

      sinon.spy(notificationManager, 'addError');

      const wrapper = shallow(app, {disableLifecycleMethods: true});

      // Committing with no staged changes should cause commit error
      try {
        await wrapper.instance().commit('message');
      } catch (e) {
        assert(e, 'is error');
      }

      assert.isTrue(notificationManager.addError.called);

      assert.strictEqual(repository.getCommitMessage(), 'some message');
    });

    describe('message formatting', function() {
      let commitSpy;
      beforeEach(function() {
        commitSpy = sinon.stub().returns(Promise.resolve());
        app = React.cloneElement(app, {commit: commitSpy});
      });

      it('wraps the commit message body at 72 characters if github.automaticCommitMessageWrapping is true', async function() {
        config.set('github.automaticCommitMessageWrapping', false);

        const wrapper = shallow(app, {disableLifecycleMethods: true});

        await wrapper.instance().commit([
          'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor',
          '',
          'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
        ].join('\n'));

        assert.deepEqual(commitSpy.args[0][0].split('\n'), [
          'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor',
          '',
          'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
        ]);

        commitSpy.reset();
        config.set('github.automaticCommitMessageWrapping', true);

        await wrapper.instance().commit([
          'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor',
          '',
          'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
        ].join('\n'));

        assert.deepEqual(commitSpy.args[0][0].split('\n'), [
          'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor',
          '',
          'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ',
          'ut aliquip ex ea commodo consequat.',
        ]);
      });
    });

    describe('toggling between commit box and commit editor', function() {
      it('transfers the commit message contents of the last editor', async function() {
        const wrapper = shallow(app, {disableLifecycleMethods: true});

        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('message in box');
        await assert.async.equal(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath());
        wrapper.update();
        assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox'));

        const editor = workspace.getActiveTextEditor();
        assert.strictEqual(editor.getText(), 'message in box');
        editor.setText('message in editor');
        await editor.save();

        workspace.paneForItem(editor).splitRight({copyActiveItem: true});
        await assert.async.notEqual(workspace.getActiveTextEditor(), editor);

        editor.destroy();
        assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox'));

        workspace.getActiveTextEditor().destroy();
        assert.isTrue(wrapper.find('CommitView').prop('deactivateCommitBox'));
        await assert.async.strictEqual(wrapper.update().find('CommitView').prop('message'), 'message in editor');
      });

      it('activates editor if already opened but in background', async function() {
        const wrapper = shallow(app, {disableLifecycleMethods: true});

        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('sup');
        await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath());
        const editor = workspace.getActiveTextEditor();

        await workspace.open(path.join(workdirPath, 'a.txt'));
        workspace.getActivePane().splitRight();
        await workspace.open(path.join(workdirPath, 'b.txt'));
        assert.notStrictEqual(workspace.getActiveTextEditor(), editor);

        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')();
        await assert.async.strictEqual(workspace.getActiveTextEditor(), editor);
      });

      it('closes all open commit message editors if one is in the foreground of a pane, prompting for unsaved changes', async function() {
        const wrapper = shallow(app, {disableLifecycleMethods: true});

        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('sup');
        await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath());

        const editor = workspace.getActiveTextEditor();
        workspace.paneForItem(editor).splitRight({copyActiveItem: true});
        assert.lengthOf(wrapper.instance().getCommitMessageEditors(), 2);

        // Activate another editor but keep commit message editor in foreground of inactive pane
        await workspace.open(path.join(workdirPath, 'a.txt'));
        assert.notStrictEqual(workspace.getActiveTextEditor(), editor);

        editor.setText('make some new changes');

        // atom internals calls `confirm` on the ApplicationDelegate instead of the atom environment
        sinon.stub(atomEnvironment.applicationDelegate, 'confirm').callsFake((options, callback) => {
          if (typeof callback === 'function') {
            callback(0); // Save
          }
          return 0; // TODO: Remove this return and typeof check once https://github.com/atom/atom/pull/16229 is on stable
        });
        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')();
        await assert.async.lengthOf(wrapper.instance().getCommitMessageEditors(), 0);
        assert.isTrue(atomEnvironment.applicationDelegate.confirm.called);
        await assert.async.strictEqual(wrapper.update().find('CommitView').prop('message'), 'make some new changes');
      });
    });

    describe('committing from commit editor', function() {
      it('uses git commit grammar in the editor', async function() {
        const wrapper = shallow(app, {disableLifecycleMethods: true});
        await atomEnvironment.packages.activatePackage('language-git');
        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')('sup');
        await assert.async.strictEqual(workspace.getActiveTextEditor().getGrammar().scopeName, COMMIT_GRAMMAR_SCOPE);
      });

      it('takes the commit message from the editor and deletes the `ATOM_COMMIT_EDITMSG` file', async function() {
        fs.writeFileSync(path.join(workdirPath, 'a.txt'), 'some changes');
        await repository.stageFiles(['a.txt']);

        app = React.cloneElement(app, {prepareToCommit: () => true, stagedChangesExist: true});
        const wrapper = shallow(app, {disableLifecycleMethods: true});

        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')();
        await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath());

        const editor = workspace.getActiveTextEditor();
        editor.setText('message in editor');
        await editor.save();
        wrapper.find('CommitView').prop('commit')('message in box');

        await assert.async.strictEqual((await repository.getLastCommit()).getMessageSubject(), 'message in editor');
        await assert.async.isFalse(fs.existsSync(wrapper.instance().getCommitMessagePath()));
      });

      it('asks user to confirm if commit editor has unsaved changes', async function() {
        app = React.cloneElement(app, {confirm, prepareToCommit: () => true, stagedChangesExist: true});
        const wrapper = shallow(app, {disableLifecycleMethods: true});

        sinon.stub(repository.git, 'commit');
        wrapper.find('CommitView').prop('toggleExpandedCommitMessageEditor')();
        await assert.async.strictEqual(workspace.getActiveTextEditor().getPath(), wrapper.instance().getCommitMessagePath());

        const editor = workspace.getActiveTextEditor();
        editor.setText('unsaved changes');
        workspace.paneForItem(editor).splitRight({copyActiveItem: true});
        await assert.async.notStrictEqual(workspace.getActiveTextEditor(), editor);
        assert.lengthOf(workspace.getTextEditors(), 2);

        confirm.returns(1); // Cancel
        wrapper.find('CommitView').prop('commit')('message in box');
        assert.strictEqual(repository.git.commit.callCount, 0);

        confirm.returns(0); // Commit
        wrapper.find('CommitView').prop('commit')('message in box');
        await assert.async.equal(repository.git.commit.callCount, 1);
        await assert.async.lengthOf(workspace.getTextEditors(), 0);
      });
    });
  });
});
<|endoftext|>