<commit_before>// Copyright 2015-present 650 Industries. All rights reserved.
/**
 * @flow
 */

'use strict';

// Set EXPO_VIEW_DIR to universe/exponent to test locally

import mkdirp from 'mkdirp';
import fs from 'fs-extra';
import path from 'path';
import rimraf from 'rimraf';
import glob from 'glob-promise';
import uuid from 'uuid';
import yesno from 'yesno';

import {
  isDirectory,
  parseSdkMajorVersion,
  saveImageToPathAsync,
  rimrafDontThrow,
} from './ExponentTools';

import * as IosPlist from './IosPlist';
import * as IosNSBundle from './IosNSBundle';
import * as IosWorkspace from './IosWorkspace';

import Api from '../Api';
import ErrorCode from '../ErrorCode';
import * as ProjectUtils from '../project/ProjectUtils';
import UserManager from '../User';
import XDLError from '../XDLError';
import StandaloneContext from './StandaloneContext';
import * as UrlUtils from '../UrlUtils';
import * as Utils from '../Utils';
import * as Versions from '../Versions';

const ANDROID_TEMPLATE_PKG = 'detach.app.template.pkg.name';
const ANDROID_TEMPLATE_COMPANY = 'detach.app.template.company.domain';
const ANDROID_TEMPLATE_NAME = 'DetachAppTemplate';

function yesnoAsync(question) {
  return new Promise(resolve => {
    yesno.ask(question, null, ok => {
      resolve(ok);
    });
  });
}

export async function detachAsync(projectRoot: string, options: any) {
  let user = await UserManager.ensureLoggedInAsync();

  if (!user) {
    throw new Error('Internal error -- somehow detach is being run in offline mode.');
  }

  let username = user.username;
  let { exp } = await ProjectUtils.readConfigJsonAsync(projectRoot);
  let experienceName = `@${username}/${exp.slug}`;
  let experienceUrl = `exp://exp.host/${experienceName}`;

  // Check to make sure project isn't fully detached already
  let hasIosDirectory = isDirectory(path.join(projectRoot, 'ios'));
  let hasAndroidDirectory = isDirectory(path.join(projectRoot, 'android'));

  if (hasIosDirectory && hasAndroidDirectory) {
    throw new XDLError(
      ErrorCode.DIRECTORY_ALREADY_EXISTS,
      'Error detaching. `ios` and `android` directories already exist.'
    );
  }

  // Project was already detached on Windows or Linux
  if (!hasIosDirectory && hasAndroidDirectory && process.platform === 'darwin') {
    let response = await yesnoAsync(
      `This will add an Xcode project and leave your existing Android project alone. Enter 'yes' to continue:`
    );
    if (!response) {
      console.log('Exiting...');
      return false;
    }
  }

  if (hasIosDirectory && !hasAndroidDirectory) {
    throw new Error('`ios` directory already exists. Please remove it and try again.');
  }

  console.log('Validating project manifest...');
  const configName = await ProjectUtils.configFilenameAsync(projectRoot);
  if (!exp.name) {
    throw new Error(`${configName} is missing \`name\``);
  }

  if (!exp.android || !exp.android.package) {
    throw new Error(
      `${configName} is missing android.package field. See https://docs.expo.io/versions/latest/guides/configuration.html#package`
    );
  }

  if (!exp.sdkVersion) {
    throw new Error(`${configName} is missing \`sdkVersion\``);
  }

  let majorSdkVersion = parseSdkMajorVersion(exp.sdkVersion);
  if (majorSdkVersion < 16) {
    throw new Error(`${configName} must be updated to SDK 16.0.0 or newer to be detached.`);
  }

  const versions = await Versions.versionsAsync();
  let sdkVersionConfig = versions.sdkVersions[exp.sdkVersion];
  if (
    !sdkVersionConfig ||
    !sdkVersionConfig.androidExpoViewUrl ||
    !sdkVersionConfig.iosExpoViewUrl
  ) {
    if (process.env.EXPO_VIEW_DIR) {
      console.warn(
        `Detaching is not supported for SDK ${exp.sdkVersion}; ignoring this because you provided EXPO_VIEW_DIR`
      );
      sdkVersionConfig = {};
    } else {
      throw new Error(`Detaching is not supported for SDK version ${exp.sdkVersion}`);
    }
  }

  // Modify exp.json
  exp.isDetached = true;

  if (!exp.detach) {
    exp.detach = {};
  }

  if (!exp.detach.scheme) {
    let detachedUUID = uuid.v4().replace(/-/g, '');
    exp.detach.scheme = `exp${detachedUUID}`;
  }

  let expoDirectory = path.join(projectRoot, '.expo-source');
  mkdirp.sync(expoDirectory);

  // iOS
  let isIosSupported = true;
  if (process.platform !== 'darwin') {
    if (options && options.force) {
      console.warn(
        `You are not running macOS, but have provided the --force option, so we will attempt to generate an iOS project anyway. This might fail.`
      );
    } else {
      console.warn(`Skipping iOS because you are not running macOS.`);
      isIosSupported = false;
    }
  }
  if (!hasIosDirectory && isIosSupported) {
    const context = StandaloneContext.createUserContext(projectRoot, exp, experienceUrl);
    await detachIOSAsync(context);
    exp = IosWorkspace.addDetachedConfigToExp(exp, context);
    exp.detach.iosExpoViewUrl = sdkVersionConfig.iosExpoViewUrl;
  }

  // Android
  if (!hasAndroidDirectory) {
    let androidDirectory = path.join(expoDirectory, 'android');
    rimraf.sync(androidDirectory);
    mkdirp.sync(androidDirectory);
    await detachAndroidAsync(
      projectRoot,
      androidDirectory,
      exp.sdkVersion,
      experienceUrl,
      exp,
      sdkVersionConfig.androidExpoViewUrl
    );
    exp.detach.androidExpoViewUrl = sdkVersionConfig.androidExpoViewUrl;
  }

  console.log('Writing ExpoKit configuration...');
  // Update exp.json/app.json
  // if we're writing to app.json, we need to place the configuration under the expo key
  const nameToWrite = await ProjectUtils.configFilenameAsync(projectRoot);
  if (nameToWrite === 'app.json') {
    exp = { expo: exp };
  }
  await fs.writeFile(path.join(projectRoot, nameToWrite), JSON.stringify(exp, null, 2));

  console.log(
    'Finished detaching your project! Look in the `android` and `ios` directories for the respective native projects. Follow the ExpoKit guide at https://docs.expo.io/versions/latest/guides/expokit.html to get your project running.\n'
  );
  return true;
}

/**
 *  Create a detached Expo iOS app pointing at the given project.
 */
async function detachIOSAsync(context: StandaloneContext) {
  await IosWorkspace.createDetachedAsync(context);

  console.log('Configuring iOS project...');
  await IosNSBundle.configureAsync(context);

  console.log(`iOS detach is complete!`);
}

async function regexFileAsync(filename, regex, replace) {
  let file = await fs.readFile(filename);
  let fileString = file.toString();
  await fs.writeFile(filename, fileString.replace(regex, replace));
}

async function renamePackageAsync(directory, originalPkg, destPkg) {
  let originalSplitPackage = originalPkg.split('.');
  let originalDeepDirectory = directory;
  for (let i = 0; i < originalSplitPackage.length; i++) {
    originalDeepDirectory = path.join(originalDeepDirectory, originalSplitPackage[i]);
  }

  // copy files into temp directory
  let tmpDirectory = path.join(directory, 'tmp-exponent-directory');
  mkdirp.sync(tmpDirectory);
  await Utils.ncpAsync(originalDeepDirectory, tmpDirectory);

  // delete old package
  rimraf.sync(path.join(directory, originalSplitPackage[0]));

  // make new package
  let newSplitPackage = destPkg.split('.');
  let newDeepDirectory = directory;
  for (let i = 0; i < newSplitPackage.length; i++) {
    newDeepDirectory = path.join(newDeepDirectory, newSplitPackage[i]);
    mkdirp.sync(newDeepDirectory);
  }

  // copy from temp to new package
  await Utils.ncpAsync(tmpDirectory, newDeepDirectory);

  // delete temp
  rimraf.sync(tmpDirectory);
}

async function detachAndroidAsync(
  projectRoot,
  expoDirectory,
  sdkVersion,
  experienceUrl,
  manifest,
  expoViewUrl: string
) {
  let tmpExpoDirectory;
  if (process.env.EXPO_VIEW_DIR) {
    // Only for testing
    tmpExpoDirectory = process.env.EXPO_VIEW_DIR;
  } else {
    tmpExpoDirectory = path.join(projectRoot, 'temp-android-directory');
    mkdirp.sync(tmpExpoDirectory);
    console.log('Downloading Android code...');
    await Api.downloadAsync(expoViewUrl, tmpExpoDirectory, { extract: true });
  }

  let androidProjectDirectory = path.join(projectRoot, 'android');

  console.log('Moving Android project files...');

  await Utils.ncpAsync(
    path.join(tmpExpoDirectory, 'android', 'maven'),
    path.join(expoDirectory, 'maven')
  );
  await Utils.ncpAsync(
    path.join(tmpExpoDirectory, 'android', 'detach-scripts'),
    path.join(expoDirectory, 'detach-scripts')
  );
  await Utils.ncpAsync(
    path.join(tmpExpoDirectory, 'exponent-view-template', 'android'),
    androidProjectDirectory
  );
  if (process.env.EXPO_VIEW_DIR) {
    rimraf.sync(path.join(androidProjectDirectory, 'build'));
    rimraf.sync(path.join(androidProjectDirectory, 'app', 'build'));
  }

  // Fix up app/build.gradle
  console.log('Configuring Android project...');
  let appBuildGradle = path.join(androidProjectDirectory, 'app', 'build.gradle');
  await regexFileAsync(appBuildGradle, /\/\* UNCOMMENT WHEN DISTRIBUTING/g, '');
  await regexFileAsync(appBuildGradle, /END UNCOMMENT WHEN DISTRIBUTING \*\//g, '');
  await regexFileAsync(appBuildGradle, `compile project(':expoview')`, '');

  // Fix AndroidManifest
  let androidManifest = path.join(
    androidProjectDirectory,
    'app',
    'src',
    'main',
    'AndroidManifest.xml'
  );
  await regexFileAsync(androidManifest, 'PLACEHOLDER_DETACH_SCHEME', manifest.detach.scheme);

  // Fix MainActivity
  let mainActivity = path.join(
    androidProjectDirectory,
    'app',
    'src',
    'main',
    'java',
    'detach',
    'app',
    'template',
    'pkg',
    'name',
    'MainActivity.java'
  );
  await regexFileAsync(mainActivity, 'TEMPLATE_INITIAL_URL', experienceUrl);

  // Fix package name
  let packageName = manifest.android.package;
  await renamePackageAsync(
    path.join(androidProjectDirectory, 'app', 'src', 'main', 'java'),
    ANDROID_TEMPLATE_PKG,
    packageName
  );
  await renamePackageAsync(
    path.join(androidProjectDirectory, 'app', 'src', 'test', 'java'),
    ANDROID_TEMPLATE_PKG,
    packageName
  );
  await renamePackageAsync(
    path.join(androidProjectDirectory, 'app', 'src', 'androidTest', 'java'),
    ANDROID_TEMPLATE_PKG,
    packageName
  );

  let packageNameMatches = await glob(androidProjectDirectory + '/**/*.@(java|gradle|xml)');
  if (packageNameMatches) {
    let oldPkgRegex = new RegExp(`${ANDROID_TEMPLATE_PKG.replace(/\./g, '\\.')}`, 'g');
    for (let i = 0; i < packageNameMatches.length; i++) {
      await regexFileAsync(packageNameMatches[i], oldPkgRegex, packageName);
    }
  }

  // Fix app name
  console.log('Naming Android project...');
  let appName = manifest.name;
  await regexFileAsync(
    path.resolve(androidProjectDirectory, 'app', 'src', 'main', 'res', 'values', 'strings.xml'),
    ANDROID_TEMPLATE_NAME,
    appName
  );

  // Fix image
  let icon = manifest.android && manifest.android.icon ? manifest.android.icon : manifest.icon;
  if (icon) {
    let iconMatches = await glob(
      path.join(androidProjectDirectory, 'app', 'src', 'main', 'res') + '/**/ic_launcher.png'
    );
    if (iconMatches) {
      for (let i = 0; i < iconMatches.length; i++) {
        await fs.unlink(iconMatches[i]);
        // TODO: make more efficient
        await saveImageToPathAsync(projectRoot, icon, iconMatches[i]);
      }
    }
  }

  // Clean up
  console.log('Cleaning up Android...');
  if (!process.env.EXPO_VIEW_DIR) {
    rimrafDontThrow(tmpExpoDirectory);
  }
  console.log('Android detach is complete!\n');
}

async function ensureBuildConstantsExistsIOSAsync(configFilePath: string) {
  // EXBuildConstants is included in newer ExpoKit projects.
  // create it if it doesn't exist.
  const doesBuildConstantsExist = fs.existsSync(
    path.join(configFilePath, 'EXBuildConstants.plist')
  );
  if (!doesBuildConstantsExist) {
    await IosPlist.createBlankAsync(configFilePath, 'EXBuildConstants');
    console.log('Created `EXBuildConstants.plist` because it did not exist yet');
  }
}

async function prepareDetachedBuildIosAsync(projectDir: string, exp: any, args: any) {
  const context = StandaloneContext.createUserContext(projectDir, exp);
  let { iosProjectDirectory, supportingDirectory } = IosWorkspace.getPaths(context);

  console.log(`Preparing iOS build at ${iosProjectDirectory}...`);
  // These files cause @providesModule naming collisions
  // but are not available until after `pod install` has run.
  let podsDirectory = path.join(iosProjectDirectory, 'Pods');
  if (!isDirectory(podsDirectory)) {
    throw new Error(`Can't find directory ${podsDirectory}, make sure you've run pod install.`);
  }
  let rnPodDirectory = path.join(podsDirectory, 'React');
  if (isDirectory(rnPodDirectory)) {
    let rnFilesToDelete = await glob(rnPodDirectory + '/**/*.@(js|json)');
    if (rnFilesToDelete) {
      for (let i = 0; i < rnFilesToDelete.length; i++) {
        await fs.unlink(rnFilesToDelete[i]);
      }
    }
  }
  // insert expo development url into iOS config
  if (!args.skipXcodeConfig) {
    // populate EXPO_RUNTIME_VERSION from ExpoKit pod version
    let expoKitVersion = '';
    const podfileLockPath = path.join(iosProjectDirectory, 'Podfile.lock');
    try {
      const podfileLock = await fs.readFile(podfileLockPath, 'utf8');
      const expoKitVersionRegex = /ExpoKit\/Core\W?\(([0-9\.]+)\)/gi;
      let match = expoKitVersionRegex.exec(podfileLock);
      expoKitVersion = match[1];
    } catch (e) {
      throw new Error(
        `Unable to read ExpoKit version from Podfile.lock. Make sure your project depends on ExpoKit. (${e})`
      );
    }

    // populate development url
    let devUrl = await UrlUtils.constructManifestUrlAsync(projectDir);

    await ensureBuildConstantsExistsIOSAsync(supportingDirectory);
    await IosPlist.modifyAsync(supportingDirectory, 'EXBuildConstants', constantsConfig => {
      constantsConfig.developmentUrl = devUrl;
      constantsConfig.EXPO_RUNTIME_VERSION = expoKitVersion;
      return constantsConfig;
    });
  }
}

export async function prepareDetachedBuildAsync(projectDir: string, args: any) {
  let { exp } = await ProjectUtils.readConfigJsonAsync(projectDir);

  if (args.platform === 'ios') {
    await prepareDetachedBuildIosAsync(projectDir, exp, args);
  } else {
    let androidProjectDirectory = path.join(projectDir, 'android');
    let expoBuildConstantsMatches = await glob(
      androidProjectDirectory + '/**/ExponentBuildConstants.java'
    );
    if (expoBuildConstantsMatches && expoBuildConstantsMatches.length) {
      let expoBuildConstants = expoBuildConstantsMatches[0];
      let devUrl = await UrlUtils.constructManifestUrlAsync(projectDir);
      await regexFileAsync(
        expoBuildConstants,
        /DEVELOPMENT_URL \= \"[^\"]*\"\;/,
        `DEVELOPMENT_URL = "${devUrl}";`
      );
    }
  }
}
<commit_msg>Use AndroidShellApp for detach<commit_after>// Copyright 2015-present 650 Industries. All rights reserved.
/**
 * @flow
 */

'use strict';

// Set EXPO_VIEW_DIR to universe/exponent to test locally

import mkdirp from 'mkdirp';
import fs from 'fs-extra';
import path from 'path';
import rimraf from 'rimraf';
import glob from 'glob-promise';
import uuid from 'uuid';
import yesno from 'yesno';

import {
  isDirectory,
  parseSdkMajorVersion,
  saveImageToPathAsync,
  rimrafDontThrow,
} from './ExponentTools';

import * as IosPlist from './IosPlist';
import * as IosNSBundle from './IosNSBundle';
import * as IosWorkspace from './IosWorkspace';
import * as AndroidShellApp from './AndroidShellApp';

import Api from '../Api';
import ErrorCode from '../ErrorCode';
import * as ProjectUtils from '../project/ProjectUtils';
import UserManager from '../User';
import XDLError from '../XDLError';
import StandaloneContext from './StandaloneContext';
import * as UrlUtils from '../UrlUtils';
import * as Utils from '../Utils';
import * as Versions from '../Versions';

function yesnoAsync(question) {
  return new Promise(resolve => {
    yesno.ask(question, null, ok => {
      resolve(ok);
    });
  });
}

export async function detachAsync(projectRoot: string, options: any) {
  let user = await UserManager.ensureLoggedInAsync();

  if (!user) {
    throw new Error('Internal error -- somehow detach is being run in offline mode.');
  }

  let username = user.username;
  let { exp } = await ProjectUtils.readConfigJsonAsync(projectRoot);
  let experienceName = `@${username}/${exp.slug}`;
  let experienceUrl = `exp://exp.host/${experienceName}`;

  // Check to make sure project isn't fully detached already
  let hasIosDirectory = isDirectory(path.join(projectRoot, 'ios'));
  let hasAndroidDirectory = isDirectory(path.join(projectRoot, 'android'));

  if (hasIosDirectory && hasAndroidDirectory) {
    throw new XDLError(
      ErrorCode.DIRECTORY_ALREADY_EXISTS,
      'Error detaching. `ios` and `android` directories already exist.'
    );
  }

  // Project was already detached on Windows or Linux
  if (!hasIosDirectory && hasAndroidDirectory && process.platform === 'darwin') {
    let response = await yesnoAsync(
      `This will add an Xcode project and leave your existing Android project alone. Enter 'yes' to continue:`
    );
    if (!response) {
      console.log('Exiting...');
      return false;
    }
  }

  if (hasIosDirectory && !hasAndroidDirectory) {
    throw new Error('`ios` directory already exists. Please remove it and try again.');
  }

  console.log('Validating project manifest...');
  const configName = await ProjectUtils.configFilenameAsync(projectRoot);
  if (!exp.name) {
    throw new Error(`${configName} is missing \`name\``);
  }

  if (!exp.android || !exp.android.package) {
    throw new Error(
      `${configName} is missing android.package field. See https://docs.expo.io/versions/latest/guides/configuration.html#package`
    );
  }

  if (!exp.sdkVersion) {
    throw new Error(`${configName} is missing \`sdkVersion\``);
  }

  let majorSdkVersion = parseSdkMajorVersion(exp.sdkVersion);
  if (majorSdkVersion < 16) {
    throw new Error(`${configName} must be updated to SDK 16.0.0 or newer to be detached.`);
  }

  const versions = await Versions.versionsAsync();
  let sdkVersionConfig = versions.sdkVersions[exp.sdkVersion];
  if (
    !sdkVersionConfig ||
    !sdkVersionConfig.androidExpoViewUrl ||
    !sdkVersionConfig.iosExpoViewUrl
  ) {
    if (process.env.EXPO_VIEW_DIR) {
      console.warn(
        `Detaching is not supported for SDK ${exp.sdkVersion}; ignoring this because you provided EXPO_VIEW_DIR`
      );
      sdkVersionConfig = {};
    } else {
      throw new Error(`Detaching is not supported for SDK version ${exp.sdkVersion}`);
    }
  }

  // Modify exp.json
  exp.isDetached = true;

  if (!exp.detach) {
    exp.detach = {};
  }

  if (!exp.detach.scheme) {
    let detachedUUID = uuid.v4().replace(/-/g, '');
    exp.detach.scheme = `exp${detachedUUID}`;
  }

  let expoDirectory = path.join(projectRoot, '.expo-source');
  mkdirp.sync(expoDirectory);

  const context = StandaloneContext.createUserContext(projectRoot, exp, experienceUrl);

  // iOS
  let isIosSupported = true;
  if (process.platform !== 'darwin') {
    if (options && options.force) {
      console.warn(
        `You are not running macOS, but have provided the --force option, so we will attempt to generate an iOS project anyway. This might fail.`
      );
    } else {
      console.warn(`Skipping iOS because you are not running macOS.`);
      isIosSupported = false;
    }
  }
  if (!hasIosDirectory && isIosSupported) {
    await detachIOSAsync(context);
    exp = IosWorkspace.addDetachedConfigToExp(exp, context);
    exp.detach.iosExpoViewUrl = sdkVersionConfig.iosExpoViewUrl;
  }

  // Android
  if (!hasAndroidDirectory) {
    let androidDirectory = path.join(expoDirectory, 'android');
    rimraf.sync(androidDirectory);
    mkdirp.sync(androidDirectory);
    await detachAndroidAsync(context, sdkVersionConfig.androidExpoViewUrl);
    exp.detach.androidExpoViewUrl = sdkVersionConfig.androidExpoViewUrl;
  }

  console.log('Writing ExpoKit configuration...');
  // Update exp.json/app.json
  // if we're writing to app.json, we need to place the configuration under the expo key
  const nameToWrite = await ProjectUtils.configFilenameAsync(projectRoot);
  if (nameToWrite === 'app.json') {
    exp = { expo: exp };
  }
  await fs.writeFile(path.join(projectRoot, nameToWrite), JSON.stringify(exp, null, 2));

  console.log(
    'Finished detaching your project! Look in the `android` and `ios` directories for the respective native projects. Follow the ExpoKit guide at https://docs.expo.io/versions/latest/guides/expokit.html to get your project running.\n'
  );
  return true;
}

/**
 *  Create a detached Expo iOS app pointing at the given project.
 */
async function detachIOSAsync(context: StandaloneContext) {
  await IosWorkspace.createDetachedAsync(context);

  console.log('Configuring iOS project...');
  await IosNSBundle.configureAsync(context);

  console.log(`iOS detach is complete!`);
}

async function regexFileAsync(filename, regex, replace) {
  let file = await fs.readFile(filename);
  let fileString = file.toString();
  await fs.writeFile(filename, fileString.replace(regex, replace));
}

async function detachAndroidAsync(context: StandaloneContext, expoViewUrl: string) {
  if (context.type !== 'user') {
    throw new Error(`detachAndroidAsync only supports user standalone contexts`);
  }

  console.log('Moving Android project files...');
  let androidProjectDirectory = path.join(context.data.projectPath, 'android');
  let tmpExpoDirectory;
  if (process.env.EXPO_VIEW_DIR) {
    // Only for testing
    await AndroidShellApp.copyInitialShellAppFilesAsync(
      path.join(process.env.EXPO_VIEW_DIR, 'android'),
      androidProjectDirectory
    );
  } else {
    tmpExpoDirectory = path.join(context.data.projectPath, 'temp-android-directory');
    mkdirp.sync(tmpExpoDirectory);
    console.log('Downloading Android code...');
    await Api.downloadAsync(expoViewUrl, tmpExpoDirectory, { extract: true });
    await Utils.ncpAsync(path.join(tmpExpoDirectory, 'android'), androidProjectDirectory);
  }

  console.log('Updating Android app...');
  await AndroidShellApp.runShellAppModificationsAsync(context);

  // Clean up
  console.log('Cleaning up Android...');
  if (!process.env.EXPO_VIEW_DIR) {
    rimrafDontThrow(tmpExpoDirectory);
  }
  console.log('Android detach is complete!\n');
}

async function ensureBuildConstantsExistsIOSAsync(configFilePath: string) {
  // EXBuildConstants is included in newer ExpoKit projects.
  // create it if it doesn't exist.
  const doesBuildConstantsExist = fs.existsSync(
    path.join(configFilePath, 'EXBuildConstants.plist')
  );
  if (!doesBuildConstantsExist) {
    await IosPlist.createBlankAsync(configFilePath, 'EXBuildConstants');
    console.log('Created `EXBuildConstants.plist` because it did not exist yet');
  }
}

async function prepareDetachedBuildIosAsync(projectDir: string, exp: any, args: any) {
  const context = StandaloneContext.createUserContext(projectDir, exp);
  let { iosProjectDirectory, supportingDirectory } = IosWorkspace.getPaths(context);

  console.log(`Preparing iOS build at ${iosProjectDirectory}...`);
  // These files cause @providesModule naming collisions
  // but are not available until after `pod install` has run.
  let podsDirectory = path.join(iosProjectDirectory, 'Pods');
  if (!isDirectory(podsDirectory)) {
    throw new Error(`Can't find directory ${podsDirectory}, make sure you've run pod install.`);
  }
  let rnPodDirectory = path.join(podsDirectory, 'React');
  if (isDirectory(rnPodDirectory)) {
    let rnFilesToDelete = await glob(rnPodDirectory + '/**/*.@(js|json)');
    if (rnFilesToDelete) {
      for (let i = 0; i < rnFilesToDelete.length; i++) {
        await fs.unlink(rnFilesToDelete[i]);
      }
    }
  }
  // insert expo development url into iOS config
  if (!args.skipXcodeConfig) {
    // populate EXPO_RUNTIME_VERSION from ExpoKit pod version
    let expoKitVersion = '';
    const podfileLockPath = path.join(iosProjectDirectory, 'Podfile.lock');
    try {
      const podfileLock = await fs.readFile(podfileLockPath, 'utf8');
      const expoKitVersionRegex = /ExpoKit\/Core\W?\(([0-9\.]+)\)/gi;
      let match = expoKitVersionRegex.exec(podfileLock);
      expoKitVersion = match[1];
    } catch (e) {
      throw new Error(
        `Unable to read ExpoKit version from Podfile.lock. Make sure your project depends on ExpoKit. (${e})`
      );
    }

    // populate development url
    let devUrl = await UrlUtils.constructManifestUrlAsync(projectDir);

    await ensureBuildConstantsExistsIOSAsync(supportingDirectory);
    await IosPlist.modifyAsync(supportingDirectory, 'EXBuildConstants', constantsConfig => {
      constantsConfig.developmentUrl = devUrl;
      constantsConfig.EXPO_RUNTIME_VERSION = expoKitVersion;
      return constantsConfig;
    });
  }
}

export async function prepareDetachedBuildAsync(projectDir: string, args: any) {
  let { exp } = await ProjectUtils.readConfigJsonAsync(projectDir);

  if (args.platform === 'ios') {
    await prepareDetachedBuildIosAsync(projectDir, exp, args);
  } else {
    let androidProjectDirectory = path.join(projectDir, 'android');
    let expoBuildConstantsMatches = await glob(
      androidProjectDirectory + '/**/ExponentBuildConstants.java'
    );
    if (expoBuildConstantsMatches && expoBuildConstantsMatches.length) {
      let expoBuildConstants = expoBuildConstantsMatches[0];
      let devUrl = await UrlUtils.constructManifestUrlAsync(projectDir);
      await regexFileAsync(
        expoBuildConstants,
        /DEVELOPMENT_URL \= \"[^\"]*\"\;/,
        `DEVELOPMENT_URL = "${devUrl}";`
      );
    }
  }
}
<|endoftext|>


<commit_before>import { diff, unmount } from './index';
import { coerceToVNode, Fragment } from '../create-element';
import { EMPTY_OBJ, EMPTY_ARR } from '../constants';

/**
 * Diff the children of a virtual node
 * @param {import('../internal').PreactElement} parentDom The DOM element whose
 * children are being diffed
 * @param {import('../internal').VNode} newParentVNode The new virtual
 * node whose children should be diff'ed against oldParentVNode
 * @param {import('../internal').VNode} oldParentVNode The old virtual
 * node whose children should be diff'ed against newParentVNode
 * @param {object} context The current context object
 * @param {boolean} isSvg Whether or not this DOM node is an SVG node
 * @param {Array<import('../internal').PreactElement>} excessDomChildren
 * @param {Array<import('../internal').Component>} mounts The list of components
 * which have mounted
 * @param {import('../internal').Component} ancestorComponent The direct parent
 * component to the ones being diffed
 */
export function diffChildren(parentDom, newParentVNode, oldParentVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent) {
	let childVNode, i, j, p, index, oldVNode, newDom,
		nextDom, sibDom, focus,
		childDom;

	let newChildren = getVNodeChildren(newParentVNode);
	let oldChildren = oldParentVNode==null || oldParentVNode==EMPTY_OBJ ? EMPTY_ARR : getVNodeChildren(oldParentVNode);

	let oldChildrenLength = oldChildren.length;

	childDom = oldChildren.length ? oldChildren[0] && oldChildren[0]._dom : null;
	if (excessDomChildren!=null) {
		for (i = 0; i < excessDomChildren.length; i++) {
			if (excessDomChildren[i]!=null) {
				childDom = excessDomChildren[i];
				break;
			}
		}
	}

	for (i=0; i<newChildren.length; i++) {
		childVNode = newChildren[i] = coerceToVNode(newChildren[i]);
		oldVNode = index = null;

		// Check if we find a corresponding element in oldChildren and store the
		// index where the element was found.
		p = oldChildren[i];
		if (p != null && (childVNode.key==null && p.key==null ? (childVNode.type === p.type) : (childVNode.key === p.key))) {
			index = i;
		}
		else {
			for (j=0; j<oldChildrenLength; j++) {
				p = oldChildren[j];
				if (p!=null) {
					if (childVNode.key==null && p.key==null ? (childVNode.type === p.type) : (childVNode.key === p.key)) {
						index = j;
						break;
					}
				}
			}
		}

		// If we have found a corresponding old element we store it in a variable
		// and delete it from the array. That way the next iteration can skip this
		// element.
		if (index!=null) {
			oldVNode = oldChildren[index];
			oldChildren[index] = null;
		}

		nextDom = childDom!=null && childDom.nextSibling;

		// Morph the old element into the new one, but don't append it to the dom yet
		newDom = diff(oldVNode==null ? null : oldVNode._dom, parentDom, childVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent);

		// Only proceed if the vnode has not been unmounted by `diff()` above.
		if (childVNode!=null && newDom !=null) {
			// Store focus in case moving children around changes it. Note that we
			// can't just check once for every tree, because we have no way to
			// differentiate wether the focus was reset by the user in a lifecycle
			// hook or by reordering dom nodes.
			focus = document.activeElement;

			if (childVNode._lastDomChild != null) {
				// Only Fragments or components that return Fragment like VNodes will
				// have a non-null _lastDomChild. Continue the diff from the end of
				// this Fragment's DOM tree.
				newDom = childVNode._lastDomChild;
			}
			else if (excessDomChildren==oldVNode || newDom!=childDom || newDom.parentNode==null) {
				// NOTE: excessDomChildren==oldVNode above:
				// This is a compression of excessDomChildren==null && oldVNode==null!
				// The values only have the same type when `null`.

				outer: if (childDom==null || childDom.parentNode!==parentDom) {
					parentDom.appendChild(newDom);
				}
				else {
					sibDom = childDom;
					j = 0;
					while ((sibDom=sibDom.nextSibling) && j++<oldChildrenLength/2) {
						if (sibDom===newDom) {
							break outer;
						}
					}
					parentDom.insertBefore(newDom, childDom);
				}
			}

			// Restore focus if it was changed
			if (focus!==document.activeElement) {
				focus.focus();
			}

			childDom = newDom!=null ? newDom.nextSibling : nextDom;
		}
	}

	// Remove children that are not part of any vnode. Only used by `hydrate`
	if (excessDomChildren!=null && newParentVNode.type!==Fragment) for (i=excessDomChildren.length; i--; ) if (excessDomChildren[i]!=null) excessDomChildren[i].remove();

	// Remove remaining oldChildren if there are any.
	for (i=oldChildren.length; i--; ) if (oldChildren[i]!=null) unmount(oldChildren[i], ancestorComponent);
}

/**
 * Get the children of a virtual node as a flat array
 * @param {import('../internal').VNode} vnode The virtual node to get the
 * children of
 * @returns {Array<import('../internal').VNode>} The virtual node's children
 */
function getVNodeChildren(vnode) {
	if (vnode._children==null) {
		toChildArray(vnode.props.children, vnode._children=[]);
	}
	return vnode._children;
}

/**
 * Flatten a virtual nodes children to a single dimensional array
 * @param {import('../index').ComponentChildren} children The unflattened
 * children of a virtual node
 * @param {Array<import('../internal').VNode | null>} [flattened] An flat array of children to modify
 */
export function toChildArray(children, flattened) {
	if (flattened===undefined) flattened = [];
	if (children==null || typeof children === 'boolean') {}
	else if (Array.isArray(children)) {
		for (let i=0; i < children.length; i++) {
			toChildArray(children[i], flattened);
		}
	}
	else {
		flattened.push(coerceToVNode(children));
	}

	return flattened;
}
<commit_msg>save ~10b by relaxing empty children -> array coersion<commit_after>import { diff, unmount } from './index';
import { coerceToVNode, Fragment } from '../create-element';
import { EMPTY_OBJ, EMPTY_ARR } from '../constants';

/**
 * Diff the children of a virtual node
 * @param {import('../internal').PreactElement} parentDom The DOM element whose
 * children are being diffed
 * @param {import('../internal').VNode} newParentVNode The new virtual
 * node whose children should be diff'ed against oldParentVNode
 * @param {import('../internal').VNode} oldParentVNode The old virtual
 * node whose children should be diff'ed against newParentVNode
 * @param {object} context The current context object
 * @param {boolean} isSvg Whether or not this DOM node is an SVG node
 * @param {Array<import('../internal').PreactElement>} excessDomChildren
 * @param {Array<import('../internal').Component>} mounts The list of components
 * which have mounted
 * @param {import('../internal').Component} ancestorComponent The direct parent
 * component to the ones being diffed
 */
export function diffChildren(parentDom, newParentVNode, oldParentVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent) {
	let childVNode, i, j, p, index, oldVNode, newDom,
		nextDom, sibDom, focus,
		childDom;

	let newChildren = getVNodeChildren(newParentVNode);
	let oldChildren = oldParentVNode==null || oldParentVNode==EMPTY_OBJ ? EMPTY_ARR : getVNodeChildren(oldParentVNode);

	let oldChildrenLength = oldChildren.length;

	childDom = oldChildren.length ? oldChildren[0] && oldChildren[0]._dom : null;
	if (excessDomChildren!=null) {
		for (i = 0; i < excessDomChildren.length; i++) {
			if (excessDomChildren[i]!=null) {
				childDom = excessDomChildren[i];
				break;
			}
		}
	}

	for (i=0; i<newChildren.length; i++) {
		childVNode = newChildren[i] = coerceToVNode(newChildren[i]);
		oldVNode = index = null;

		// Check if we find a corresponding element in oldChildren and store the
		// index where the element was found.
		p = oldChildren[i];
		if (p != null && (childVNode.key==null && p.key==null ? (childVNode.type === p.type) : (childVNode.key === p.key))) {
			index = i;
		}
		else {
			for (j=0; j<oldChildrenLength; j++) {
				p = oldChildren[j];
				if (p!=null) {
					if (childVNode.key==null && p.key==null ? (childVNode.type === p.type) : (childVNode.key === p.key)) {
						index = j;
						break;
					}
				}
			}
		}

		// If we have found a corresponding old element we store it in a variable
		// and delete it from the array. That way the next iteration can skip this
		// element.
		if (index!=null) {
			oldVNode = oldChildren[index];
			oldChildren[index] = null;
		}

		nextDom = childDom!=null && childDom.nextSibling;

		// Morph the old element into the new one, but don't append it to the dom yet
		newDom = diff(oldVNode==null ? null : oldVNode._dom, parentDom, childVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent);

		// Only proceed if the vnode has not been unmounted by `diff()` above.
		if (childVNode!=null && newDom !=null) {
			// Store focus in case moving children around changes it. Note that we
			// can't just check once for every tree, because we have no way to
			// differentiate wether the focus was reset by the user in a lifecycle
			// hook or by reordering dom nodes.
			focus = document.activeElement;

			if (childVNode._lastDomChild != null) {
				// Only Fragments or components that return Fragment like VNodes will
				// have a non-null _lastDomChild. Continue the diff from the end of
				// this Fragment's DOM tree.
				newDom = childVNode._lastDomChild;
			}
			else if (excessDomChildren==oldVNode || newDom!=childDom || newDom.parentNode==null) {
				// NOTE: excessDomChildren==oldVNode above:
				// This is a compression of excessDomChildren==null && oldVNode==null!
				// The values only have the same type when `null`.

				outer: if (childDom==null || childDom.parentNode!==parentDom) {
					parentDom.appendChild(newDom);
				}
				else {
					sibDom = childDom;
					j = 0;
					while ((sibDom=sibDom.nextSibling) && j++<oldChildrenLength/2) {
						if (sibDom===newDom) {
							break outer;
						}
					}
					parentDom.insertBefore(newDom, childDom);
				}
			}

			// Restore focus if it was changed
			if (focus!==document.activeElement) {
				focus.focus();
			}

			childDom = newDom!=null ? newDom.nextSibling : nextDom;
		}
	}

	// Remove children that are not part of any vnode. Only used by `hydrate`
	if (excessDomChildren!=null && newParentVNode.type!==Fragment) for (i=excessDomChildren.length; i--; ) if (excessDomChildren[i]!=null) excessDomChildren[i].remove();

	// Remove remaining oldChildren if there are any.
	for (i=oldChildren.length; i--; ) if (oldChildren[i]!=null) unmount(oldChildren[i], ancestorComponent);
}

/**
 * Get the children of a virtual node as a flat array
 * @param {import('../internal').VNode} vnode The virtual node to get the
 * children of
 * @returns {Array<import('../internal').VNode>} The virtual node's children
 */
function getVNodeChildren(vnode) {
	if (vnode._children==null) {
		toChildArray(vnode.props.children, vnode._children=[]);
	}
	return vnode._children;
}

/**
 * Flatten a virtual nodes children to a single dimensional array
 * @param {import('../index').ComponentChildren} children The unflattened
 * children of a virtual node
 * @param {Array<import('../internal').VNode | null>} [flattened] An flat array of children to modify
 */
export function toChildArray(children, flattened) {
	if (flattened == null) flattened = [];
	if (children==null || typeof children === 'boolean') {}
	else if (Array.isArray(children)) {
		for (let i=0; i < children.length; i++) {
			toChildArray(children[i], flattened);
		}
	}
	else {
		flattened.push(coerceToVNode(children));
	}

	return flattened;
}
<|endoftext|>


<commit_before>//>>excludeStart("exclude", pragmas.exclude);
define([ "shoestring" ], function(){
//>>excludeEnd("exclude");

	shoestring.fn.wrapInner = function( html ){
		return this.each(function(){
			var inH = this.innerHTML;

			this.innerHTML = "";
			shoestring( this ).append( shoestring( html ).html( inH ) );
		});
	};

//>>excludeStart("exclude", pragmas.exclude);
});
//>>excludeEnd("exclude");
<commit_msg>doc wrapInner<commit_after>//>>excludeStart("exclude", pragmas.exclude);
define([ "shoestring" ], function(){
//>>excludeEnd("exclude");

	/**
	 * Wraps the child elements in the provided HTML.
	 *
	 * @param {string} html The wrapping HTML.
	 * @return shoestring
	 * @this shoestring
	 */
	shoestring.fn.wrapInner = function( html ){
		return this.each(function(){
			var inH = this.innerHTML;

			this.innerHTML = "";
			shoestring( this ).append( shoestring( html ).html( inH ) );
		});
	};

//>>excludeStart("exclude", pragmas.exclude);
});
//>>excludeEnd("exclude");
<|endoftext|>


<commit_before>/* globals jQuery, window, document */

(function($) {

    var methods = {
        options : {
            "optionClass": "",
            "dropdownClass": "",
            "autoinit": false,
            callback: false
        },
        init: function(options) {

            // Apply user options if user has defined some
            if (options) {
                options = $.extend(methods.options, options);
            } else {
                options = methods.options;
            }

            function initElement($select) {
                // Don't do anything if this is not a select or if this select was already initialized
                if ($select.data("dropdownjs") || !$select.is("select")) {
                    return;
                }

                // Is it a multi select?
                var multi = $select.attr("multiple");

                // Create the dropdown wrapper
                var $dropdown = $("

");
                $dropdown.addClass("dropdownjs").addClass(options.dropdownStyle);
                $dropdown.data("select", $select);

                // Create the fake input used as "select" element and cache it as $input
                var $input = $("<input type=text readonly>");
                // Append it to the dropdown wrapper
                $dropdown.append($input);

                // Create the UL that will be used as dropdown and cache it AS $ul
                var $ul = $("

");

                // Append it to the dropdown
                $dropdown.append($ul);

                // Transfer the placeholder attribute
                $input.attr("placeholder", $select.attr("placeholder"));

                // Loop trough options and transfer them to the dropdown menu
                $select.find("option").each(function() {
                    // Cache $(this)
                    var $this = $(this);

                    // Create the option
                    var $option = $("

");

                    // Style the option
                    $option.addClass(options.optionStyle);

                    // If the option has some text then transfer it
                    if ($this.text()) {
                        $option.text($this.text());
                    }
                    // Otherwise set the empty label and set it as an empty option
                    else {
                        $option.html(" ");
                    }
                    // Set the value of the option
                    $option.attr("value", $this.val());

                    // Ss it selected?
                    if ($this.attr("selected")) {
                        $option.attr("selected", true);
                    }

                    // Append option to our dropdown
                    $ul.append($option);
                });

                // Cache the dropdown options
                var selectOptions = $dropdown.find("li");

                // If is a single select, selected the first one or the last with selected attribute
                if (!multi) {
                    var $selected;
                    if ($ul.find("[selected]").length) {
                        $selected = $ul.find("[selected]").last();
                    }
                    else {
                        $selected = $ul.find("li").first();
                    }
                    methods._select($dropdown, $selected);
                } else {
                    methods._select($dropdown, $ul.find("[selected]"));
                }

                // Transfer the classes of the select to the input dropdown
                $input.addClass($select[0].className);

                // Hide the old and ugly select
                $select.hide().data("dropdownjs", true);

                // Bring to life our awesome dropdownjs
                $select.after($dropdown);

                // Call the callback
                if (options.callback) {
                    options.callback($dropdown);
                }

                //---------------------------------------//
                // DROPDOWN EVENTS                       //
                //---------------------------------------//

                // On click, set the clicked one as selected
                selectOptions.on("click", function(e) {
                    methods._select($dropdown, $(this));
                });
                selectOptions.on("keydown", function(e) {
                    if (e.which === 27) {
                        $(".dropdownjs > ul > li").each(function() { $(this).attr("tabindex", -1); });
                        return $input.removeClass("focus").blur();
                    }
                    if (e.which === 32) {
                        methods._select($dropdown, $(this));
                        return false;
                    }
                });

                selectOptions.on("focus", function() {
                    $input.addClass("focus");
                });

                // Used to make the dropdown menu more dropdown-ish
                $input.on("click focus", function(e) {
                    e.stopPropagation();
                    $(".dropdownjs > ul > li").each(function() { $(this).attr("tabindex", -1); });
                    $(".dropdownjs > input").not($(this)).removeClass("focus").blur();

                    selectOptions.each(function() { $(this).attr("tabindex", 0); });

                    // Set height of the dropdown
                    var windowH = $(window).height(),
                        scrollTop = $(window).scrollTop(),
                        offset = $(this).offset();

                    var height = windowH - offset.top;

                    // Decide if place the dropdown below or above the input
                    if (height < 80 && windowH > (offset.top - scrollTop)) {
                        height = offset.top - scrollTop;
                        $ul.attr("placement", "top-left");
                    } else {
                        $ul.attr("placement", "bottom-left");
                    }

                    $(this).next("ul").css("max-height", height - 20);
                    $(this).addClass("focus");
                });
                $(document).on("click", function() {
                    $(".dropdownjs > ul > li").each(function() { $(this).attr("tabindex", -1); });
                    $input.removeClass("focus");
                });
            }

            if (options.autoinit) {
                $(document).on("DOMNodeInserted", function(e) {
                    var $this = $(e.target);
                    if ($this.is("select") && $this.is(options.autoinit)) {
                        initElement($this);
                    }
                });
            }

            // Loop trough elements
            $(this).each(function() {
                initElement($(this));
            });
        },
        select: function(target) {
            var $target = $(this).find("[value=\"" + target + "\"]");
            methods._select($(this), $target);
        },
        _select: function($dropdown, $target) {
            // Get dropdown's elements
            var $select = $dropdown.data("select"),
                $input  = $dropdown.find("input");

            // Is it a multi select?
            var multi = $select.attr("multiple");

            // Cache the dropdown options
            var selectOptions = $dropdown.find("li");

            // Behavior for multiple select
            if (multi) {
                // Toggle option state
                $target.toggleClass("selected");
                // Toggle selection of the clicked option in native select
                var $selected = $select.find("[value=\"" + $target.attr("value") + "\"]");
                if ($selected.attr("selected")) {
                    $selected.attr("selected", true);
                } else {
                    $selected.attr("selected", false);
                }
                // Add or remove the value from the input
                var text = [];
                selectOptions.each(function() {
                    if ($(this).hasClass("selected")) {
                        text.push($(this).text());
                    }
                });
                $input.val(text.join(", "));
            }

            // Behavior for single select
            if (!multi) {
                // Unselect options except the one that will be selected
                selectOptions.not($target).removeClass("selected");
                // Select the selected option
                $target.addClass("selected");
                // Set the value to the native select
                $select.val($target.attr("value"));
                // Set the value to the input
                $input.val($target.text());
            }

            // This is used only if Material Design for Bootstrap is selected
            if ($.material) {
                if ($input.val().trim()) {
                    $select.removeClass("empty");
                } else {
                    $select.addClass("empty");
                }
            }

        }
    };

    $.fn.dropdown = function(params) {
        if (methods[params]) {
            return methods[params].apply(this, Array.prototype.slice.call(arguments,1));
        } else if (typeof params === "object" | !params) {
            return methods.init.apply(this, arguments);
        } else {
            $.error("Method " + params + " does not exists on jQuery.dropdown");
        }
    };

})(jQuery);
<commit_msg>added quotes<commit_after>/* globals jQuery, window, document */

(function($) {

    var methods = {
        options : {
            "optionClass": "",
            "dropdownClass": "",
            "autoinit": false,
            "callback": false
        },
        init: function(options) {

            // Apply user options if user has defined some
            if (options) {
                options = $.extend(methods.options, options);
            } else {
                options = methods.options;
            }

            function initElement($select) {
                // Don't do anything if this is not a select or if this select was already initialized
                if ($select.data("dropdownjs") || !$select.is("select")) {
                    return;
                }

                // Is it a multi select?
                var multi = $select.attr("multiple");

                // Create the dropdown wrapper
                var $dropdown = $("

");
                $dropdown.addClass("dropdownjs").addClass(options.dropdownStyle);
                $dropdown.data("select", $select);

                // Create the fake input used as "select" element and cache it as $input
                var $input = $("<input type=text readonly>");
                // Append it to the dropdown wrapper
                $dropdown.append($input);

                // Create the UL that will be used as dropdown and cache it AS $ul
                var $ul = $("

");

                // Append it to the dropdown
                $dropdown.append($ul);

                // Transfer the placeholder attribute
                $input.attr("placeholder", $select.attr("placeholder"));

                // Loop trough options and transfer them to the dropdown menu
                $select.find("option").each(function() {
                    // Cache $(this)
                    var $this = $(this);

                    // Create the option
                    var $option = $("

");

                    // Style the option
                    $option.addClass(options.optionStyle);

                    // If the option has some text then transfer it
                    if ($this.text()) {
                        $option.text($this.text());
                    }
                    // Otherwise set the empty label and set it as an empty option
                    else {
                        $option.html(" ");
                    }
                    // Set the value of the option
                    $option.attr("value", $this.val());

                    // Ss it selected?
                    if ($this.attr("selected")) {
                        $option.attr("selected", true);
                    }

                    // Append option to our dropdown
                    $ul.append($option);
                });

                // Cache the dropdown options
                var selectOptions = $dropdown.find("li");

                // If is a single select, selected the first one or the last with selected attribute
                if (!multi) {
                    var $selected;
                    if ($ul.find("[selected]").length) {
                        $selected = $ul.find("[selected]").last();
                    }
                    else {
                        $selected = $ul.find("li").first();
                    }
                    methods._select($dropdown, $selected);
                } else {
                    methods._select($dropdown, $ul.find("[selected]"));
                }

                // Transfer the classes of the select to the input dropdown
                $input.addClass($select[0].className);

                // Hide the old and ugly select
                $select.hide().data("dropdownjs", true);

                // Bring to life our awesome dropdownjs
                $select.after($dropdown);

                // Call the callback
                if (options.callback) {
                    options.callback($dropdown);
                }

                //---------------------------------------//
                // DROPDOWN EVENTS                       //
                //---------------------------------------//

                // On click, set the clicked one as selected
                selectOptions.on("click", function(e) {
                    methods._select($dropdown, $(this));
                });
                selectOptions.on("keydown", function(e) {
                    if (e.which === 27) {
                        $(".dropdownjs > ul > li").each(function() { $(this).attr("tabindex", -1); });
                        return $input.removeClass("focus").blur();
                    }
                    if (e.which === 32) {
                        methods._select($dropdown, $(this));
                        return false;
                    }
                });

                selectOptions.on("focus", function() {
                    $input.addClass("focus");
                });

                // Used to make the dropdown menu more dropdown-ish
                $input.on("click focus", function(e) {
                    e.stopPropagation();
                    $(".dropdownjs > ul > li").each(function() { $(this).attr("tabindex", -1); });
                    $(".dropdownjs > input").not($(this)).removeClass("focus").blur();

                    selectOptions.each(function() { $(this).attr("tabindex", 0); });

                    // Set height of the dropdown
                    var windowH = $(window).height(),
                        scrollTop = $(window).scrollTop(),
                        offset = $(this).offset();

                    var height = windowH - offset.top;

                    // Decide if place the dropdown below or above the input
                    if (height < 80 && windowH > (offset.top - scrollTop)) {
                        height = offset.top - scrollTop;
                        $ul.attr("placement", "top-left");
                    } else {
                        $ul.attr("placement", "bottom-left");
                    }

                    $(this).next("ul").css("max-height", height - 20);
                    $(this).addClass("focus");
                });
                $(document).on("click", function() {
                    $(".dropdownjs > ul > li").each(function() { $(this).attr("tabindex", -1); });
                    $input.removeClass("focus");
                });
            }

            if (options.autoinit) {
                $(document).on("DOMNodeInserted", function(e) {
                    var $this = $(e.target);
                    if ($this.is("select") && $this.is(options.autoinit)) {
                        initElement($this);
                    }
                });
            }

            // Loop trough elements
            $(this).each(function() {
                initElement($(this));
            });
        },
        select: function(target) {
            var $target = $(this).find("[value=\"" + target + "\"]");
            methods._select($(this), $target);
        },
        _select: function($dropdown, $target) {
            // Get dropdown's elements
            var $select = $dropdown.data("select"),
                $input  = $dropdown.find("input");

            // Is it a multi select?
            var multi = $select.attr("multiple");

            // Cache the dropdown options
            var selectOptions = $dropdown.find("li");

            // Behavior for multiple select
            if (multi) {
                // Toggle option state
                $target.toggleClass("selected");
                // Toggle selection of the clicked option in native select
                var $selected = $select.find("[value=\"" + $target.attr("value") + "\"]");
                if ($selected.attr("selected")) {
                    $selected.attr("selected", true);
                } else {
                    $selected.attr("selected", false);
                }
                // Add or remove the value from the input
                var text = [];
                selectOptions.each(function() {
                    if ($(this).hasClass("selected")) {
                        text.push($(this).text());
                    }
                });
                $input.val(text.join(", "));
            }

            // Behavior for single select
            if (!multi) {
                // Unselect options except the one that will be selected
                selectOptions.not($target).removeClass("selected");
                // Select the selected option
                $target.addClass("selected");
                // Set the value to the native select
                $select.val($target.attr("value"));
                // Set the value to the input
                $input.val($target.text());
            }

            // This is used only if Material Design for Bootstrap is selected
            if ($.material) {
                if ($input.val().trim()) {
                    $select.removeClass("empty");
                } else {
                    $select.addClass("empty");
                }
            }

        }
    };

    $.fn.dropdown = function(params) {
        if (methods[params]) {
            return methods[params].apply(this, Array.prototype.slice.call(arguments,1));
        } else if (typeof params === "object" | !params) {
            return methods.init.apply(this, arguments);
        } else {
            $.error("Method " + params + " does not exists on jQuery.dropdown");
        }
    };

})(jQuery);
<|endoftext|>


<commit_before>var shuffle = require('knuth-shuffle');
var Promise = require('native-promise-only');
var minimatch = require('minimatch');

var BlameRange = require('./models/blame-range');
var PullRequestFile = require('./models/pull-request-file');

/**
 * Compute the relevant reviewers of a pull request
 * @param  {Object} options
 * @param  {Object} options.config - Pull Review configuration object
 * @param  {Array}  options.files - list of GitHub pull request files
 * @param  {Array}  options.commits - list of pull request commits
 * @param  {Array}  options.assignees - list of pull request assignees
 * @param  {String} options.authorLogin - username of the pull request author
 * @param  {Function} options.getBlameForFile - function that returns Git blame data for pull request file
 * @param  {Boolean} options.retryReview - unassign current reviewers and assign new reviewers excluding previous reviewers
 * @return {Array} list of reviewers
 */
module.exports = function getReviewers(options) {
  options = options || {};
  var config = options.config || {};
  var files = options.files || [];
  var commits = options.commits || [];
  var assignees = options.assignees || [];
  var labels = options.labels || [];
  var authorLogin = options.authorLogin;
  var getBlameForFile = options.getBlameForFile;
  var retryReview = Boolean(options.retryReview);

  if (!getBlameForFile) {
    throw Error('No function provided for retrieving blame for a file');
  } else if (!authorLogin) {
    throw Error('No pull request author provided');
  }

  var maxReviewers = config.maxReviewers;
  var minReviewers = config.minReviewers;
  var maxFilesPerReviewer = config.maxFilesPerReviewer;
  var maxLinesPerReviewer = config.maxLinesPerReviewer;
  var minAuthorsOfChangedFiles = config.minAuthorsOfChangedFiles;
  var minLinesChangedForExtraReviewer = config.minLinesChangedForExtraReviewer;
  var minPercentAuthorshipForExtraReviewer = config.minPercentAuthorshipForExtraReviewer;
  var maxReviewersAssignedDynamically =
    maxFilesPerReviewer > 0 || maxLinesPerReviewer > 0;
  var fileBlacklist = config.fileBlacklist;
  var reviewPathFallbacks = config.reviewPathFallbacks;
  var reviewPathAssignments = config.reviewPathAssignments;
  var labelWhitelist = config.labelWhitelist;
  var labelBlacklist = config.labelBlacklist;

  if (labels.length) {
    for (var i = 0; i < labels.length; i++) {
      var label = labels[i].name;
      if (
        (labelBlacklist.length && labelBlacklist.indexOf(label) !== -1) ||
        (labelWhitelist.length && labelWhitelist.indexOf(label) === -1)
      ) {
        throw Error('Label ' + label + ' is not allowed');
      }
    }
  }

  files = files.map(PullRequestFile);

  if (fileBlacklist.length) {
    fileBlacklist.forEach(function(pattern) {
      files = files.filter(function(file) {
        return !minimatch(file.filename, pattern, {
          dot: true,
          matchBase: true
        });
      });
    });
  }

  var nonRemovedFiles = files.filter(function(file) {
    return file.status !== 'removed';
  });

  var changedLines = Math.abs(
    nonRemovedFiles.reduce(function(sum, file) {
      return sum + ((file.additions || 0) - (file.deletions || 0));
    }, 0)
  );

  var modifiedFiles = files.filter(function(file) {
    return file.status === 'modified';
  });

  modifiedFiles.sort(function(a, b) {
    return b.changes - a.changes;
  });

  var topModifiedFiles = modifiedFiles.slice(0, config.maxFiles);

  var selectedReviewers = {};
  var excludedReviewers = {};
  var currentCommitters = {};
  var uniqueAuthors = 0;

  commits.forEach(function(commit) {
    if (commit.author && commit.author.login) {
      currentCommitters[commit.author.login] = true;
    }
  });

  if (retryReview) {
    assignees.forEach(function(assignee) {
      excludedReviewers[assignee] = true;
    });

    assignees = [];
  }

  assignees = assignees.filter(function(assignee) {
    return assignee !== authorLogin;
  });

  if (assignees.length >= maxReviewers) {
    throw Error('Pull request has maximum reviewers assigned');
  } else if (assignees.length >= minReviewers) {
    throw Error('Pull request has minimum reviewers assigned');
  }

  var unassignedReviewers = maxReviewers - assignees.length;
  var maxNeededReviewers = unassignedReviewers;

  var maxReviewersUsingLines =
    maxLinesPerReviewer > 0 ? Math.ceil(changedLines / maxLinesPerReviewer) : 0;
  var maxReviewersUsingFiles =
    maxFilesPerReviewer > 0 ? Math.ceil(files.length / maxFilesPerReviewer) : 0;

  if (maxReviewersAssignedDynamically) {
    if (!maxFilesPerReviewer && maxLinesPerReviewer) {
      maxNeededReviewers = maxReviewersUsingLines;
    } else if (!maxLinesPerReviewer && maxFilesPerReviewer) {
      maxNeededReviewers = maxReviewersUsingFiles;
    } else {
      maxNeededReviewers = Math.min(
        maxReviewersUsingLines,
        maxReviewersUsingFiles
      );
    }

    maxNeededReviewers = Math.max(minReviewers, maxNeededReviewers);
  }

  var maxReviewersAssignable = Math.min(
    unassignedReviewers,
    maxNeededReviewers
  );
  var minReviewersAssignable = maxReviewersAssignedDynamically
    ? maxReviewersAssignable
    : minReviewers;

  /**
   * @param  {String} reviewer - reviewer username
   * @return {Boolean} is reviewer eligible for this review request?
   */
  function isEligibleReviewer(reviewer) {
    var isReviewerSelected = selectedReviewers[reviewer];
    var isReviewerCurrentCommitter = currentCommitters[reviewer];
    var isReviewerAuthor = reviewer === authorLogin;
    var isReviewerUnreachable =
      config.requireNotification && !config.reviewers[reviewer];
    var isReviewerBlacklisted =
      config.reviewBlacklist && config.reviewBlacklist.indexOf(reviewer) !== -1;
    var isReviewerExcluded = excludedReviewers[reviewer];
    return (
      !isReviewerCurrentCommitter &&
      !isReviewerUnreachable &&
      !isReviewerBlacklisted &&
      !isReviewerExcluded &&
      !isReviewerSelected &&
      !isReviewerAuthor
    );
  }

  /**
   * @return {Array} reviewers found for fallback paths
   */
  function getFallbackReviewers() {
    var fallbackReviewers = [];

    Object.keys(reviewPathFallbacks || {}).forEach(function(pattern) {
      var matchingFiles = files.filter(function(file) {
        return minimatch(file.filename, pattern, {
          dot: true,
          matchBase: true
        });
      });

      matchingFiles.forEach(function() {
        var fallbackAuthors = reviewPathFallbacks[pattern];

        fallbackAuthors.forEach(function(author) {
          if (!isEligibleReviewer(author)) {
            return;
          }

          fallbackReviewers.push({
            login: author,
            count: 0,
            source: 'fallback'
          });

          selectedReviewers[author] = true;
        });
      });
    });

    shuffle.knuthShuffle(fallbackReviewers);
    return fallbackReviewers;
  }

  /**
   * @return {Array} reviewers picked at random
   */
  function getRandomReviewers() {
    var randomReviewers = [];

    Object.keys(config.reviewers).forEach(function(author) {
      if (!isEligibleReviewer(author)) {
        return;
      }

      randomReviewers.push({
        login: author,
        count: 0,
        source: 'random'
      });

      selectedReviewers[author] = true;
    });

    shuffle.knuthShuffle(randomReviewers);
    return randomReviewers;
  }

  var assignedReviewers = [];

  Object.keys(reviewPathAssignments || {}).forEach(function(pattern) {
    var matchingFiles = files.filter(function(file) {
      return minimatch(file.filename, pattern, {
        dot: true,
        matchBase: true
      });
    });

    matchingFiles.forEach(function() {
      var assignedAuthors = reviewPathAssignments[pattern] || [];

      assignedAuthors.forEach(function(author) {
        if (!isEligibleReviewer(author)) {
          return;
        }

        assignedReviewers.push({
          login: author,
          count: 0,
          source: 'assignment'
        });

        selectedReviewers[author] = true;
      });
    });
  });

  shuffle.knuthShuffle(assignedReviewers);

  return Promise.all(topModifiedFiles.map(getBlameForFile))
    .then(function(fileBlames) {
      var authorsLinesChanged = {};
      var filesWithOwnership = [];
      var authorOwnership = {};

      for (var i = 0; i < fileBlames.length; i++) {
        var ranges = (fileBlames[i] || []).map(BlameRange);

        ranges.sort(function(a, b) {
          return a.age - b.age;
        });

        var eligibleBlames = ranges.filter(function(range) {
          return isEligibleReviewer(range.login);
        });

        var recentBlames = eligibleBlames.slice(
          0,
          Math.ceil(eligibleBlames.length * 0.75)
        );

        var file = {
          authors: {},
          lines: 0
        };

        recentBlames.forEach(function(range) {
          var linesChanged = range.count;
          var author = range.login;

          if (!authorsLinesChanged[author]) {
            authorsLinesChanged[author] = 0;
          }

          file.authors[author] = file.authors[author] || 0;
          authorsLinesChanged[author] += linesChanged;
          file.authors[author] += linesChanged;
          file.lines += linesChanged;
        });

        if (file.lines) {
          filesWithOwnership.push(file);
        }
      }

      var reviewersByOwnership = [];

      filesWithOwnership.forEach(function(file) {
        Object.keys(file.authors).forEach(function(author) {
          if (!authorOwnership[author]) {
            authorOwnership[author] = [];
          }

          authorOwnership[author].push(
            Number((file.authors[author] / file.lines).toFixed(3))
          );
        });
      });

      uniqueAuthors = Object.keys(authorOwnership).length;

      //todo: consider decoupling author ownership calculation from reviewer eligibility
      //for example, an un-assigned reviewer has still contributed to author ownership
      //of the files in a PR, so it would be somewhat inaccurate to assume that author ownership
      //is less diverse just because a reviewer has been excluded
      Object.keys(authorOwnership).forEach(function(author) {
        var ownershipPercentages = authorOwnership[author];
        var averageOwnership =
          ownershipPercentages.reduce(function(sum, ownership) {
            return sum + ownership;
          }, 0) / filesWithOwnership.length;

        reviewersByOwnership.push({
          login: author,
          count: authorsLinesChanged[author],
          source: 'blame',
          ownership: Number(averageOwnership.toFixed(3))
        });
      });

      reviewersByOwnership.sort(function(a, b) {
        return b.count * b.ownership - a.count * a.ownership;
      });

      return assignedReviewers.concat(reviewersByOwnership);
    })
    .then(function(allReviewers) {
      var reviewers = allReviewers.slice(0, maxReviewersAssignable);

      reviewers.forEach(function(reviewer) {
        selectedReviewers[reviewer.login] = true;
      });

      var fallbackReviewers = getFallbackReviewers();
      var randomReviewers = getRandomReviewers();
      var extraReviewers = fallbackReviewers.concat(randomReviewers);
      var notEnoughAuthorDiversity = minAuthorsOfChangedFiles > 0 && uniqueAuthors < minAuthorsOfChangedFiles;
      var tooMuchAuthorship = minPercentAuthorshipForExtraReviewer > 0 && reviewers.length && !isNaN(reviewers[0].ownership) && (reviewers[0].ownership * 100) >= minPercentAuthorshipForExtraReviewer;
      var extraReviewerToAssign = null;

      if (maxReviewers > 1) {
        if (notEnoughAuthorDiversity && !tooMuchAuthorship) {
          var needExtraReviewer = minLinesChangedForExtraReviewer > 0 ? changedLines >= minLinesChangedForExtraReviewer : true;
          if (needExtraReviewer) {
            extraReviewerToAssign = 'fallback';
          }
        } else if (tooMuchAuthorship) {
          extraReviewerToAssign = 'next-best';
        }
      }

      if (extraReviewerToAssign) {
        var extraReviewer;

        if (extraReviewerToAssign === 'fallback') {
          extraReviewer = extraReviewers.shift();
        } else if (extraReviewerToAssign === 'next-best' && allReviewers.length > reviewers.length) {
          extraReviewer = allReviewers.slice(maxReviewersAssignable).shift();
        }

        if (extraReviewer) {
          if (reviewers.length < maxReviewers) {
            reviewers.push(extraReviewer);
          } else if (reviewers.length === maxReviewers) {
            reviewers = reviewers.slice(0, -1).concat(extraReviewer);
          }
          excludedReviewers[extraReviewer.login] = true;
        }
      }

      if (
        reviewers.length < minReviewersAssignable &&
        config.assignMinReviewersRandomly
      ) {
        reviewers = reviewers.concat(
          extraReviewers.slice(0, minReviewersAssignable - reviewers.length)
        );
      }

      return reviewers;
    })
    .then(function(reviewers) {
      return reviewers.map(function(reviewer) {
        reviewer.notify = config.reviewers[reviewer.login];
        return reviewer;
      });
    });
};
<commit_msg>only use min changed lines if there is not too much authorship<commit_after>var shuffle = require('knuth-shuffle');
var Promise = require('native-promise-only');
var minimatch = require('minimatch');

var BlameRange = require('./models/blame-range');
var PullRequestFile = require('./models/pull-request-file');

/**
 * Compute the relevant reviewers of a pull request
 * @param  {Object} options
 * @param  {Object} options.config - Pull Review configuration object
 * @param  {Array}  options.files - list of GitHub pull request files
 * @param  {Array}  options.commits - list of pull request commits
 * @param  {Array}  options.assignees - list of pull request assignees
 * @param  {String} options.authorLogin - username of the pull request author
 * @param  {Function} options.getBlameForFile - function that returns Git blame data for pull request file
 * @param  {Boolean} options.retryReview - unassign current reviewers and assign new reviewers excluding previous reviewers
 * @return {Array} list of reviewers
 */
module.exports = function getReviewers(options) {
  options = options || {};
  var config = options.config || {};
  var files = options.files || [];
  var commits = options.commits || [];
  var assignees = options.assignees || [];
  var labels = options.labels || [];
  var authorLogin = options.authorLogin;
  var getBlameForFile = options.getBlameForFile;
  var retryReview = Boolean(options.retryReview);

  if (!getBlameForFile) {
    throw Error('No function provided for retrieving blame for a file');
  } else if (!authorLogin) {
    throw Error('No pull request author provided');
  }

  var maxReviewers = config.maxReviewers;
  var minReviewers = config.minReviewers;
  var maxFilesPerReviewer = config.maxFilesPerReviewer;
  var maxLinesPerReviewer = config.maxLinesPerReviewer;
  var minAuthorsOfChangedFiles = config.minAuthorsOfChangedFiles;
  var minLinesChangedForExtraReviewer = config.minLinesChangedForExtraReviewer;
  var minPercentAuthorshipForExtraReviewer = config.minPercentAuthorshipForExtraReviewer;
  var maxReviewersAssignedDynamically =
    maxFilesPerReviewer > 0 || maxLinesPerReviewer > 0;
  var fileBlacklist = config.fileBlacklist;
  var reviewPathFallbacks = config.reviewPathFallbacks;
  var reviewPathAssignments = config.reviewPathAssignments;
  var labelWhitelist = config.labelWhitelist;
  var labelBlacklist = config.labelBlacklist;

  if (labels.length) {
    for (var i = 0; i < labels.length; i++) {
      var label = labels[i].name;
      if (
        (labelBlacklist.length && labelBlacklist.indexOf(label) !== -1) ||
        (labelWhitelist.length && labelWhitelist.indexOf(label) === -1)
      ) {
        throw Error('Label ' + label + ' is not allowed');
      }
    }
  }

  files = files.map(PullRequestFile);

  if (fileBlacklist.length) {
    fileBlacklist.forEach(function(pattern) {
      files = files.filter(function(file) {
        return !minimatch(file.filename, pattern, {
          dot: true,
          matchBase: true
        });
      });
    });
  }

  var nonRemovedFiles = files.filter(function(file) {
    return file.status !== 'removed';
  });

  var changedLines = Math.abs(
    nonRemovedFiles.reduce(function(sum, file) {
      return sum + ((file.additions || 0) - (file.deletions || 0));
    }, 0)
  );

  var modifiedFiles = files.filter(function(file) {
    return file.status === 'modified';
  });

  modifiedFiles.sort(function(a, b) {
    return b.changes - a.changes;
  });

  var topModifiedFiles = modifiedFiles.slice(0, config.maxFiles);

  var selectedReviewers = {};
  var excludedReviewers = {};
  var currentCommitters = {};
  var uniqueAuthors = 0;

  commits.forEach(function(commit) {
    if (commit.author && commit.author.login) {
      currentCommitters[commit.author.login] = true;
    }
  });

  if (retryReview) {
    assignees.forEach(function(assignee) {
      excludedReviewers[assignee] = true;
    });

    assignees = [];
  }

  assignees = assignees.filter(function(assignee) {
    return assignee !== authorLogin;
  });

  if (assignees.length >= maxReviewers) {
    throw Error('Pull request has maximum reviewers assigned');
  } else if (assignees.length >= minReviewers) {
    throw Error('Pull request has minimum reviewers assigned');
  }

  var unassignedReviewers = maxReviewers - assignees.length;
  var maxNeededReviewers = unassignedReviewers;

  var maxReviewersUsingLines =
    maxLinesPerReviewer > 0 ? Math.ceil(changedLines / maxLinesPerReviewer) : 0;
  var maxReviewersUsingFiles =
    maxFilesPerReviewer > 0 ? Math.ceil(files.length / maxFilesPerReviewer) : 0;

  if (maxReviewersAssignedDynamically) {
    if (!maxFilesPerReviewer && maxLinesPerReviewer) {
      maxNeededReviewers = maxReviewersUsingLines;
    } else if (!maxLinesPerReviewer && maxFilesPerReviewer) {
      maxNeededReviewers = maxReviewersUsingFiles;
    } else {
      maxNeededReviewers = Math.min(
        maxReviewersUsingLines,
        maxReviewersUsingFiles
      );
    }

    maxNeededReviewers = Math.max(minReviewers, maxNeededReviewers);
  }

  var maxReviewersAssignable = Math.min(
    unassignedReviewers,
    maxNeededReviewers
  );
  var minReviewersAssignable = maxReviewersAssignedDynamically
    ? maxReviewersAssignable
    : minReviewers;

  /**
   * @param  {String} reviewer - reviewer username
   * @return {Boolean} is reviewer eligible for this review request?
   */
  function isEligibleReviewer(reviewer) {
    var isReviewerSelected = selectedReviewers[reviewer];
    var isReviewerCurrentCommitter = currentCommitters[reviewer];
    var isReviewerAuthor = reviewer === authorLogin;
    var isReviewerUnreachable =
      config.requireNotification && !config.reviewers[reviewer];
    var isReviewerBlacklisted =
      config.reviewBlacklist && config.reviewBlacklist.indexOf(reviewer) !== -1;
    var isReviewerExcluded = excludedReviewers[reviewer];
    return (
      !isReviewerCurrentCommitter &&
      !isReviewerUnreachable &&
      !isReviewerBlacklisted &&
      !isReviewerExcluded &&
      !isReviewerSelected &&
      !isReviewerAuthor
    );
  }

  /**
   * @return {Array} reviewers found for fallback paths
   */
  function getFallbackReviewers() {
    var fallbackReviewers = [];

    Object.keys(reviewPathFallbacks || {}).forEach(function(pattern) {
      var matchingFiles = files.filter(function(file) {
        return minimatch(file.filename, pattern, {
          dot: true,
          matchBase: true
        });
      });

      matchingFiles.forEach(function() {
        var fallbackAuthors = reviewPathFallbacks[pattern];

        fallbackAuthors.forEach(function(author) {
          if (!isEligibleReviewer(author)) {
            return;
          }

          fallbackReviewers.push({
            login: author,
            count: 0,
            source: 'fallback'
          });

          selectedReviewers[author] = true;
        });
      });
    });

    shuffle.knuthShuffle(fallbackReviewers);
    return fallbackReviewers;
  }

  /**
   * @return {Array} reviewers picked at random
   */
  function getRandomReviewers() {
    var randomReviewers = [];

    Object.keys(config.reviewers).forEach(function(author) {
      if (!isEligibleReviewer(author)) {
        return;
      }

      randomReviewers.push({
        login: author,
        count: 0,
        source: 'random'
      });

      selectedReviewers[author] = true;
    });

    shuffle.knuthShuffle(randomReviewers);
    return randomReviewers;
  }

  var assignedReviewers = [];

  Object.keys(reviewPathAssignments || {}).forEach(function(pattern) {
    var matchingFiles = files.filter(function(file) {
      return minimatch(file.filename, pattern, {
        dot: true,
        matchBase: true
      });
    });

    matchingFiles.forEach(function() {
      var assignedAuthors = reviewPathAssignments[pattern] || [];

      assignedAuthors.forEach(function(author) {
        if (!isEligibleReviewer(author)) {
          return;
        }

        assignedReviewers.push({
          login: author,
          count: 0,
          source: 'assignment'
        });

        selectedReviewers[author] = true;
      });
    });
  });

  shuffle.knuthShuffle(assignedReviewers);

  return Promise.all(topModifiedFiles.map(getBlameForFile))
    .then(function(fileBlames) {
      var authorsLinesChanged = {};
      var filesWithOwnership = [];
      var authorOwnership = {};

      for (var i = 0; i < fileBlames.length; i++) {
        var ranges = (fileBlames[i] || []).map(BlameRange);

        ranges.sort(function(a, b) {
          return a.age - b.age;
        });

        var eligibleBlames = ranges.filter(function(range) {
          return isEligibleReviewer(range.login);
        });

        var recentBlames = eligibleBlames.slice(
          0,
          Math.ceil(eligibleBlames.length * 0.75)
        );

        var file = {
          authors: {},
          lines: 0
        };

        recentBlames.forEach(function(range) {
          var linesChanged = range.count;
          var author = range.login;

          if (!authorsLinesChanged[author]) {
            authorsLinesChanged[author] = 0;
          }

          file.authors[author] = file.authors[author] || 0;
          authorsLinesChanged[author] += linesChanged;
          file.authors[author] += linesChanged;
          file.lines += linesChanged;
        });

        if (file.lines) {
          filesWithOwnership.push(file);
        }
      }

      var reviewersByOwnership = [];

      filesWithOwnership.forEach(function(file) {
        Object.keys(file.authors).forEach(function(author) {
          if (!authorOwnership[author]) {
            authorOwnership[author] = [];
          }

          authorOwnership[author].push(
            Number((file.authors[author] / file.lines).toFixed(3))
          );
        });
      });

      uniqueAuthors = Object.keys(authorOwnership).length;

      //todo: consider decoupling author ownership calculation from reviewer eligibility
      //for example, an un-assigned reviewer has still contributed to author ownership
      //of the files in a PR, so it would be somewhat inaccurate to assume that author ownership
      //is less diverse just because a reviewer has been excluded
      Object.keys(authorOwnership).forEach(function(author) {
        var ownershipPercentages = authorOwnership[author];
        var averageOwnership =
          ownershipPercentages.reduce(function(sum, ownership) {
            return sum + ownership;
          }, 0) / filesWithOwnership.length;

        reviewersByOwnership.push({
          login: author,
          count: authorsLinesChanged[author],
          source: 'blame',
          ownership: Number(averageOwnership.toFixed(3))
        });
      });

      reviewersByOwnership.sort(function(a, b) {
        return b.count * b.ownership - a.count * a.ownership;
      });

      return assignedReviewers.concat(reviewersByOwnership);
    })
    .then(function(allReviewers) {
      var reviewers = allReviewers.slice(0, maxReviewersAssignable);

      reviewers.forEach(function(reviewer) {
        selectedReviewers[reviewer.login] = true;
      });

      var fallbackReviewers = getFallbackReviewers();
      var randomReviewers = getRandomReviewers();
      var extraReviewers = fallbackReviewers.concat(randomReviewers);
      var notEnoughAuthorDiversity = minAuthorsOfChangedFiles > 0 && uniqueAuthors < minAuthorsOfChangedFiles;
      var tooMuchAuthorship = minPercentAuthorshipForExtraReviewer > 0 && reviewers.length && !isNaN(reviewers[0].ownership) && (reviewers[0].ownership * 100) >= minPercentAuthorshipForExtraReviewer;
      var extraReviewerToAssign = null;

      if (maxReviewers > 1) {
        if (notEnoughAuthorDiversity) {
          var needExtraReviewer = (minLinesChangedForExtraReviewer > 0 && !tooMuchAuthorship) ? changedLines >= minLinesChangedForExtraReviewer : true;
          if (needExtraReviewer) {
            extraReviewerToAssign = 'fallback';
          }
        } else if (tooMuchAuthorship) {
          extraReviewerToAssign = 'next-best';
        }
      }

      if (extraReviewerToAssign) {
        var extraReviewer;

        if (extraReviewerToAssign === 'fallback') {
          extraReviewer = extraReviewers.shift();
        } else if (extraReviewerToAssign === 'next-best' && allReviewers.length > reviewers.length) {
          extraReviewer = allReviewers.slice(maxReviewersAssignable).shift();
        }

        if (extraReviewer) {
          if (reviewers.length < maxReviewers) {
            reviewers.push(extraReviewer);
          } else if (reviewers.length === maxReviewers) {
            reviewers = reviewers.slice(0, -1).concat(extraReviewer);
          }
          excludedReviewers[extraReviewer.login] = true;
        }
      }

      if (
        reviewers.length < minReviewersAssignable &&
        config.assignMinReviewersRandomly
      ) {
        reviewers = reviewers.concat(
          extraReviewers.slice(0, minReviewersAssignable - reviewers.length)
        );
      }

      return reviewers;
    })
    .then(function(reviewers) {
      return reviewers.map(function(reviewer) {
        reviewer.notify = config.reviewers[reviewer.login];
        return reviewer;
      });
    });
};
<|endoftext|>


<commit_before>'use strict' /* @flow */

import tinycolor from 'tinycolor2'

export default {

  simpleCheckForValidColor (data) {
    var keysToCheck = ['r', 'g', 'b', 'a', 'h', 's', 'a', 'v']
    var checked = 0
    var passed = 0
    for (var i = 0; i < keysToCheck.length; i++) {
      var letter = keysToCheck[i]
      if (data[letter]) {
        checked++
        if (!isNaN(data[letter])) {
          passed++
        }
      }
    }

    if (checked === passed) {
      return data
    }
  },

  toState (data, oldHue) {
    var color = data.hex ? tinycolor(data.hex) : tinycolor(data)
    var hsl = color.toHsl()
    var hsv = color.toHsv()
    if (hsl.s === 0) {
      hsl.h = oldHue || 0
      hsv.h = oldHue || 0
    }

    return {
      hsl: hsl,
      hex: `#${ color.toHex() }`,
      rgb: color.toRgb(),
      hsv: hsv,
      oldHue: data.h || oldHue || hsl.h,
      source: data.source,
    }
  },

  isValidHex (hex) {
    return tinycolor(hex).isValid()
  },

}
<commit_msg>[UPD] delete helpers color<commit_after><|endoftext|>